/* * Copyright (C) 2021 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.safetycenter; import static android.Manifest.permission.MANAGE_SAFETY_CENTER; import static android.Manifest.permission.READ_SAFETY_CENTER_STATUS; import static android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE; import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION; import static android.os.Build.VERSION_CODES.TIRAMISU; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static java.util.Objects.requireNonNull; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.RemoteException; import android.safetycenter.config.SafetyCenterConfig; import android.util.ArrayMap; import androidx.annotation.RequiresApi; import com.android.internal.annotations.GuardedBy; import com.android.modules.utils.build.SdkLevel; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; /** * Interface for communicating with the Safety Center, which consolidates UI for security and * privacy features on the device. * *

These APIs are intended to be used by the following clients: * *

* * @hide */ @SystemService(Context.SAFETY_CENTER_SERVICE) @SystemApi @RequiresApi(TIRAMISU) public final class SafetyCenterManager { /** * Broadcast Action: A broadcast sent by the system to indicate that the value returned by * {@link SafetyCenterManager#isSafetyCenterEnabled()} has changed. * *

This broadcast will inform receivers about changes to {@link * SafetyCenterManager#isSafetyCenterEnabled()}, should they want to check the new value and * enable/disable components accordingly. * *

This broadcast is sent explicitly to safety sources by targeting intents to a specified * set of packages in the {@link SafetyCenterConfig}. The receiving components must hold the * {@link android.Manifest.permission#SEND_SAFETY_CENTER_UPDATE} permission, and can use a * manifest-registered receiver to be woken up by Safety Center. * *

This broadcast is also sent implicitly system-wide. The receiving components must hold the * {@link android.Manifest.permission#READ_SAFETY_CENTER_STATUS} permission. * *

This broadcast is not sent out if the device does not support Safety Center. * *

This is a protected intent that can only be sent by the system. */ @SdkConstant(BROADCAST_INTENT_ACTION) public static final String ACTION_SAFETY_CENTER_ENABLED_CHANGED = "android.safetycenter.action.SAFETY_CENTER_ENABLED_CHANGED"; /** * Broadcast Action: A broadcast sent by the system to indicate that {@link SafetyCenterManager} * is requesting data from safety sources regarding their safety state. * *

This broadcast is sent when a user triggers a data refresh from the Safety Center UI or * when Safety Center detects that its stored safety information is stale and needs to be * updated. * *

This broadcast is sent explicitly to safety sources by targeting intents to a specified * set of packages provided by the safety sources in the {@link SafetyCenterConfig}. The * receiving components must hold the {@link * android.Manifest.permission#SEND_SAFETY_CENTER_UPDATE} permission, and can use a * manifest-registered receiver to be woken up by Safety Center. * *

On receiving this broadcast, safety sources should determine their safety state according * to the parameters specified in the intent extras (see below) and set {@link SafetySourceData} * using {@link #setSafetySourceData}, along with a {@link SafetyEvent} with {@link * SafetyEvent#getType()} set to {@link SafetyEvent#SAFETY_EVENT_TYPE_REFRESH_REQUESTED} and * {@link SafetyEvent#getRefreshBroadcastId()} set to the value of broadcast intent extra {@link * #EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID}. If the safety source is unable to provide data, * it can set a {@code null} {@link SafetySourceData}, which will clear any existing {@link * SafetySourceData} stored by Safety Center, and Safety Center will fall back to any * placeholder data specified in {@link SafetyCenterConfig}. * *

This is a protected intent that can only be sent by the system. * *

Includes the following extras: * *

*/ @SdkConstant(BROADCAST_INTENT_ACTION) public static final String ACTION_REFRESH_SAFETY_SOURCES = "android.safetycenter.action.REFRESH_SAFETY_SOURCES"; /** * Used as a {@code String[]} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to * specify the safety source ids of the safety sources being requested for data by Safety * Center. * *

When this extra field is not specified in the intent, it is assumed that Safety Center is * requesting data from all safety sources supported by the component receiving the broadcast. */ public static final String EXTRA_REFRESH_SAFETY_SOURCE_IDS = "android.safetycenter.extra.REFRESH_SAFETY_SOURCE_IDS"; /** * Used as an {@code int} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to * specify the type of data request from Safety Center. * *

Possible values are {@link #EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA} and {@link * #EXTRA_REFRESH_REQUEST_TYPE_GET_DATA} */ public static final String EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE = "android.safetycenter.extra.REFRESH_SAFETY_SOURCES_REQUEST_TYPE"; /** * Used as a {@code String} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to * specify a string identifier for the broadcast. */ public static final String EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID = "android.safetycenter.extra.REFRESH_SAFETY_SOURCES_BROADCAST_ID"; /** * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to * specify an issue ID to redirect to, if applicable. * *

This extra must be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ID} as an issue ID * does not uniquely identify a {@link SafetySourceIssue}. Otherwise, no redirection will occur. */ public static final String EXTRA_SAFETY_SOURCE_ISSUE_ID = "android.safetycenter.extra.SAFETY_SOURCE_ISSUE_ID"; /** * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to * specify a source ID for the {@link SafetySourceIssue} to redirect to, if applicable. * *

This extra must be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ISSUE_ID}. * Otherwise, no redirection will occur. */ public static final String EXTRA_SAFETY_SOURCE_ID = "android.safetycenter.extra.SAFETY_SOURCE_ID"; /** * Used as a {@link android.os.UserHandle} extra field in {@link Intent#ACTION_SAFETY_CENTER} * intents to specify a user for a given {@link SafetySourceIssue} to redirect to, if * applicable. * *

This extra can be used if the same issue ID is created for multiple users (e.g. to * disambiguate personal profile vs. managed profiles issues). * *

This extra can be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ISSUE_ID} and * {@link #EXTRA_SAFETY_SOURCE_ID}. Otherwise, the device's primary user will be used. */ public static final String EXTRA_SAFETY_SOURCE_USER_HANDLE = "android.safetycenter.extra.SAFETY_SOURCE_USER_HANDLE"; /** * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to * specify the ID for a group of safety sources. If applicable, this will redirect to the * group's corresponding subpage in the UI. */ @RequiresApi(UPSIDE_DOWN_CAKE) public static final String EXTRA_SAFETY_SOURCES_GROUP_ID = "android.safetycenter.extra.SAFETY_SOURCES_GROUP_ID"; /** * Used as an int value for {@link #EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE} to indicate that * the safety source should fetch fresh data relating to their safety state upon receiving a * broadcast with intent action {@link #ACTION_REFRESH_SAFETY_SOURCES} and provide it to Safety * Center. * *

The term "fresh" here means that the sources should ensure that the safety data is * accurate as possible at the time of providing it to Safety Center, even if it involves * performing an expensive and/or slow process. */ public static final int EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA = 0; /** * Used as an int value for {@link #EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE} to indicate that * upon receiving a broadcast with intent action {@link #ACTION_REFRESH_SAFETY_SOURCES}, the * safety source should provide data relating to their safety state to Safety Center. * *

If the source already has its safety data cached, it may provide it without triggering a * process to fetch state which may be expensive and/or slow. */ public static final int EXTRA_REFRESH_REQUEST_TYPE_GET_DATA = 1; /** * All possible types of data refresh requests in broadcasts with intent action {@link * #ACTION_REFRESH_SAFETY_SOURCES}. * * @hide */ @IntDef( prefix = {"EXTRA_REFRESH_REQUEST_TYPE_"}, value = { EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA, EXTRA_REFRESH_REQUEST_TYPE_GET_DATA, }) @Retention(RetentionPolicy.SOURCE) public @interface RefreshRequestType {} /** Indicates that the Safety Center UI has been opened by the user. */ public static final int REFRESH_REASON_PAGE_OPEN = 100; /** Indicates that the rescan button in the Safety Center UI has been clicked on by the user. */ public static final int REFRESH_REASON_RESCAN_BUTTON_CLICK = 200; /** Indicates that the device was rebooted. */ public static final int REFRESH_REASON_DEVICE_REBOOT = 300; /** Indicates that the device locale was changed. */ public static final int REFRESH_REASON_DEVICE_LOCALE_CHANGE = 400; /** Indicates that the Safety Center feature was enabled. */ public static final int REFRESH_REASON_SAFETY_CENTER_ENABLED = 500; /** Indicates a generic reason for Safety Center refresh. */ public static final int REFRESH_REASON_OTHER = 600; /** Indicates a periodic background refresh. */ @RequiresApi(UPSIDE_DOWN_CAKE) public static final int REFRESH_REASON_PERIODIC = 700; /** * The reason for requesting a refresh of {@link SafetySourceData} from safety sources. * * @hide */ @IntDef( prefix = {"REFRESH_REASON_"}, value = { REFRESH_REASON_PAGE_OPEN, REFRESH_REASON_RESCAN_BUTTON_CLICK, REFRESH_REASON_DEVICE_REBOOT, REFRESH_REASON_DEVICE_LOCALE_CHANGE, REFRESH_REASON_SAFETY_CENTER_ENABLED, REFRESH_REASON_OTHER, REFRESH_REASON_PERIODIC }) @Retention(RetentionPolicy.SOURCE) @TargetApi(UPSIDE_DOWN_CAKE) public @interface RefreshReason {} /** Listener for changes to {@link SafetyCenterData}. */ public interface OnSafetyCenterDataChangedListener { /** * Called when {@link SafetyCenterData} tracked by the manager changes. * * @param data the updated data */ void onSafetyCenterDataChanged(@NonNull SafetyCenterData data); /** * Called when the Safety Center should display an error related to changes in its data. * * @param errorDetails details of an error that should be displayed to the user */ default void onError(@NonNull SafetyCenterErrorDetails errorDetails) {} } private final Object mListenersLock = new Object(); @GuardedBy("mListenersLock") private final Map mListenersToDelegates = new ArrayMap<>(); @NonNull private final Context mContext; @NonNull private final ISafetyCenterManager mService; /** * Creates a new instance of the {@link SafetyCenterManager}. * * @param context the {@link Context} * @param service the {@link ISafetyCenterManager} service * @hide */ public SafetyCenterManager(@NonNull Context context, @NonNull ISafetyCenterManager service) { this.mContext = context; this.mService = service; } /** * Returns whether the Safety Center feature is enabled. * *

If this returns {@code false}, all the other methods in this class will no-op and/or * return default values. */ @RequiresPermission(anyOf = {READ_SAFETY_CENTER_STATUS, SEND_SAFETY_CENTER_UPDATE}) public boolean isSafetyCenterEnabled() { try { return mService.isSafetyCenterEnabled(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Set the latest {@link SafetySourceData} for a safety source, to be displayed in Safety Center * UI. * *

Each {@code safetySourceId} uniquely identifies the {@link SafetySourceData} for the * calling user. * *

This call will rewrite any existing {@link SafetySourceData} already set for the given * {@code safetySourceId} for the calling user. * * @param safetySourceId the unique identifier for a safety source in the calling user * @param safetySourceData the latest safety data for the safety source in the calling user. If * a safety source does not have any data to set, it can set its {@link SafetySourceData} to * {@code null}, in which case Safety Center will fall back to any placeholder data * specified in the safety source xml configuration. * @param safetyEvent the event that triggered the safety source to set safety data */ @RequiresPermission(SEND_SAFETY_CENTER_UPDATE) public void setSafetySourceData( @NonNull String safetySourceId, @Nullable SafetySourceData safetySourceData, @NonNull SafetyEvent safetyEvent) { requireNonNull(safetySourceId, "safetySourceId cannot be null"); requireNonNull(safetyEvent, "safetyEvent cannot be null"); try { mService.setSafetySourceData( safetySourceId, safetySourceData, safetyEvent, mContext.getPackageName(), mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns the latest {@link SafetySourceData} set through {@link #setSafetySourceData} for the * given {@code safetySourceId} and calling user. * *

Returns {@code null} if there never was any data sent for the given {@code safetySourceId} * and user. */ @RequiresPermission(SEND_SAFETY_CENTER_UPDATE) @Nullable public SafetySourceData getSafetySourceData(@NonNull String safetySourceId) { requireNonNull(safetySourceId, "safetySourceId cannot be null"); try { return mService.getSafetySourceData( safetySourceId, mContext.getPackageName(), mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Notifies the Safety Center of an error related to a given safety source. * *

Safety sources should use this API to notify Safety Center when Safety Center requested or * expected them to perform an action or provide data, but they were unable to do so. * * @param safetySourceId the id of the safety source that provided the issue * @param safetySourceErrorDetails details of the error that occurred */ @RequiresPermission(SEND_SAFETY_CENTER_UPDATE) public void reportSafetySourceError( @NonNull String safetySourceId, @NonNull SafetySourceErrorDetails safetySourceErrorDetails) { requireNonNull(safetySourceId, "safetySourceId cannot be null"); requireNonNull(safetySourceErrorDetails, "safetySourceErrorDetails cannot be null"); try { mService.reportSafetySourceError( safetySourceId, safetySourceErrorDetails, mContext.getPackageName(), mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Requests safety sources to set their latest {@link SafetySourceData} for Safety Center. * *

This API sends a broadcast to all safety sources with action {@link * #ACTION_REFRESH_SAFETY_SOURCES}. See {@link #ACTION_REFRESH_SAFETY_SOURCES} for details on * how safety sources should respond to receiving these broadcasts. * * @param refreshReason the reason for the refresh */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void refreshSafetySources(@RefreshReason int refreshReason) { try { mService.refreshSafetySources(refreshReason, mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Requests a specific subset of safety sources to set their latest {@link SafetySourceData} for * Safety Center. * *

This API sends a broadcast to safety sources with action {@link * #ACTION_REFRESH_SAFETY_SOURCES} and {@link #EXTRA_REFRESH_SAFETY_SOURCE_IDS} to specify the * IDs of safety sources being requested for data by Safety Center. * *

This API is an overload of {@link #refreshSafetySources(int)} and is used to request data * from safety sources that are part of a subpage in the Safety Center UI. * * @see #refreshSafetySources(int) * @param refreshReason the reason for the refresh * @param safetySourceIds list of IDs for the safety sources being refreshed * @throws UnsupportedOperationException if accessed from a version lower than {@link * UPSIDE_DOWN_CAKE} */ @RequiresPermission(MANAGE_SAFETY_CENTER) @RequiresApi(UPSIDE_DOWN_CAKE) public void refreshSafetySources( @RefreshReason int refreshReason, @NonNull List safetySourceIds) { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException( "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); } requireNonNull(safetySourceIds, "safetySourceIds cannot be null"); try { mService.refreshSpecificSafetySources( refreshReason, mContext.getUser().getIdentifier(), safetySourceIds); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** Returns the current {@link SafetyCenterConfig}, if available. */ @RequiresPermission(MANAGE_SAFETY_CENTER) @Nullable public SafetyCenterConfig getSafetyCenterConfig() { try { return mService.getSafetyCenterConfig(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns the current {@link SafetyCenterData}, assembled from {@link SafetySourceData} from * all sources. */ @RequiresPermission(MANAGE_SAFETY_CENTER) @NonNull public SafetyCenterData getSafetyCenterData() { try { return mService.getSafetyCenterData( mContext.getPackageName(), mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Adds a listener for changes to {@link SafetyCenterData}. * * @see #removeOnSafetyCenterDataChangedListener(OnSafetyCenterDataChangedListener) */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void addOnSafetyCenterDataChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnSafetyCenterDataChangedListener listener) { requireNonNull(executor, "executor cannot be null"); requireNonNull(listener, "listener cannot be null"); synchronized (mListenersLock) { if (mListenersToDelegates.containsKey(listener)) return; ListenerDelegate delegate = new ListenerDelegate(executor, listener); try { mService.addOnSafetyCenterDataChangedListener( delegate, mContext.getPackageName(), mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } mListenersToDelegates.put(listener, delegate); } } /** * Removes a listener for changes to {@link SafetyCenterData}. * * @see #addOnSafetyCenterDataChangedListener(Executor, OnSafetyCenterDataChangedListener) */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void removeOnSafetyCenterDataChangedListener( @NonNull OnSafetyCenterDataChangedListener listener) { requireNonNull(listener, "listener cannot be null"); synchronized (mListenersLock) { ListenerDelegate delegate = mListenersToDelegates.get(listener); if (delegate == null) return; try { mService.removeOnSafetyCenterDataChangedListener( delegate, mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } delegate.markAsRemoved(); mListenersToDelegates.remove(listener); } } /** * Dismiss a Safety Center issue and prevent it from affecting the overall safety status. * * @param safetyCenterIssueId the target issue ID returned by {@link SafetyCenterIssue#getId()} */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void dismissSafetyCenterIssue(@NonNull String safetyCenterIssueId) { requireNonNull(safetyCenterIssueId, "safetyCenterIssueId cannot be null"); try { mService.dismissSafetyCenterIssue( safetyCenterIssueId, mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Executes the specified Safety Center issue action on the specified Safety Center issue. * * @param safetyCenterIssueId the target issue ID returned by {@link SafetyCenterIssue#getId()} * @param safetyCenterIssueActionId the target action ID returned by {@link * SafetyCenterIssue.Action#getId()} */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void executeSafetyCenterIssueAction( @NonNull String safetyCenterIssueId, @NonNull String safetyCenterIssueActionId) { requireNonNull(safetyCenterIssueId, "safetyCenterIssueId cannot be null"); requireNonNull(safetyCenterIssueActionId, "safetyCenterIssueActionId cannot be null"); try { mService.executeSafetyCenterIssueAction( safetyCenterIssueId, safetyCenterIssueActionId, mContext.getUser().getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Clears all {@link SafetySourceData} (set by safety sources using {@link * #setSafetySourceData}) for testing. * *

Note: This API serves to facilitate CTS testing and should not be used for other purposes. */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void clearAllSafetySourceDataForTests() { try { mService.clearAllSafetySourceDataForTests(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Overrides the {@link SafetyCenterConfig} for testing. * *

When set, the overridden {@link SafetyCenterConfig} will be used instead of the {@link * SafetyCenterConfig} parsed from the XML file to read configured safety sources. * *

Note: This API serves to facilitate CTS testing and should not be used to configure safety * sources dynamically for production. Once used for testing, the override should be cleared. * * @see #clearSafetyCenterConfigForTests() */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void setSafetyCenterConfigForTests(@NonNull SafetyCenterConfig safetyCenterConfig) { requireNonNull(safetyCenterConfig, "safetyCenterConfig cannot be null"); try { mService.setSafetyCenterConfigForTests(safetyCenterConfig); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Clears the override of the {@link SafetyCenterConfig} set for testing. * *

Once cleared, the {@link SafetyCenterConfig} parsed from the XML file will be used to read * configured safety sources. * *

Note: This API serves to facilitate CTS testing and should not be used for other purposes. * * @see #setSafetyCenterConfigForTests(SafetyCenterConfig) */ @RequiresPermission(MANAGE_SAFETY_CENTER) public void clearSafetyCenterConfigForTests() { try { mService.clearSafetyCenterConfigForTests(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private static final class ListenerDelegate extends IOnSafetyCenterDataChangedListener.Stub { @NonNull private final Executor mExecutor; @NonNull private final OnSafetyCenterDataChangedListener mOriginalListener; private volatile boolean mRemoved = false; private ListenerDelegate( @NonNull Executor executor, @NonNull OnSafetyCenterDataChangedListener originalListener) { mExecutor = executor; mOriginalListener = originalListener; } @Override public void onSafetyCenterDataChanged(@NonNull SafetyCenterData safetyCenterData) { requireNonNull(safetyCenterData, "safetyCenterData cannot be null"); final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute( () -> { if (mRemoved) { return; } // The remove call could still complete at this point, but we're ok // with this raciness on separate threads. If the listener is removed on // the same thread as `mExecutor`; the above check should ensure that // the listener won't be called after the remove call. mOriginalListener.onSafetyCenterDataChanged(safetyCenterData); }); } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onError(@NonNull SafetyCenterErrorDetails safetyCenterErrorDetails) { requireNonNull(safetyCenterErrorDetails, "safetyCenterErrorDetails cannot be null"); final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute( () -> { if (mRemoved) { return; } // The remove call could still complete at this point, but we're ok // with this raciness on separate threads. If the listener is removed on // the same thread as `mExecutor`; the above check should ensure that // the listener won't be called after the remove call. mOriginalListener.onError(safetyCenterErrorDetails); }); } finally { Binder.restoreCallingIdentity(identity); } } public void markAsRemoved() { mRemoved = true; } } }