1 /*
2  * Copyright (C) 2021 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 android.safetycenter;
18 
19 import static android.Manifest.permission.MANAGE_SAFETY_CENTER;
20 import static android.Manifest.permission.READ_SAFETY_CENTER_STATUS;
21 import static android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE;
22 import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION;
23 import static android.os.Build.VERSION_CODES.TIRAMISU;
24 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
25 
26 import static java.util.Objects.requireNonNull;
27 
28 import android.annotation.CallbackExecutor;
29 import android.annotation.IntDef;
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.annotation.RequiresPermission;
33 import android.annotation.SdkConstant;
34 import android.annotation.SystemApi;
35 import android.annotation.SystemService;
36 import android.annotation.TargetApi;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.os.Binder;
40 import android.os.RemoteException;
41 import android.safetycenter.config.SafetyCenterConfig;
42 import android.util.ArrayMap;
43 
44 import androidx.annotation.RequiresApi;
45 
46 import com.android.internal.annotations.GuardedBy;
47 import com.android.modules.utils.build.SdkLevel;
48 
49 import java.lang.annotation.Retention;
50 import java.lang.annotation.RetentionPolicy;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.concurrent.Executor;
54 
55 /**
56  * Interface for communicating with the Safety Center, which consolidates UI for security and
57  * privacy features on the device.
58  *
59  * <p>These APIs are intended to be used by the following clients:
60  *
61  * <ul>
62  *   <li>Safety sources represented in Safety Center UI
63  *   <li>Dependents on the state of Safety Center UI
64  *   <li>Managers of Safety Center UI
65  * </ul>
66  *
67  * @hide
68  */
69 @SystemService(Context.SAFETY_CENTER_SERVICE)
70 @SystemApi
71 @RequiresApi(TIRAMISU)
72 public final class SafetyCenterManager {
73 
74     /**
75      * Broadcast Action: A broadcast sent by the system to indicate that the value returned by
76      * {@link SafetyCenterManager#isSafetyCenterEnabled()} has changed.
77      *
78      * <p>This broadcast will inform receivers about changes to {@link
79      * SafetyCenterManager#isSafetyCenterEnabled()}, should they want to check the new value and
80      * enable/disable components accordingly.
81      *
82      * <p>This broadcast is sent explicitly to safety sources by targeting intents to a specified
83      * set of packages in the {@link SafetyCenterConfig}. The receiving components must hold the
84      * {@link android.Manifest.permission#SEND_SAFETY_CENTER_UPDATE} permission, and can use a
85      * manifest-registered receiver to be woken up by Safety Center.
86      *
87      * <p>This broadcast is also sent implicitly system-wide. The receiving components must hold the
88      * {@link android.Manifest.permission#READ_SAFETY_CENTER_STATUS} permission.
89      *
90      * <p>This broadcast is not sent out if the device does not support Safety Center.
91      *
92      * <p class="note">This is a protected intent that can only be sent by the system.
93      */
94     @SdkConstant(BROADCAST_INTENT_ACTION)
95     public static final String ACTION_SAFETY_CENTER_ENABLED_CHANGED =
96             "android.safetycenter.action.SAFETY_CENTER_ENABLED_CHANGED";
97 
98     /**
99      * Broadcast Action: A broadcast sent by the system to indicate that {@link SafetyCenterManager}
100      * is requesting data from safety sources regarding their safety state.
101      *
102      * <p>This broadcast is sent when a user triggers a data refresh from the Safety Center UI or
103      * when Safety Center detects that its stored safety information is stale and needs to be
104      * updated.
105      *
106      * <p>This broadcast is sent explicitly to safety sources by targeting intents to a specified
107      * set of packages provided by the safety sources in the {@link SafetyCenterConfig}. The
108      * receiving components must hold the {@link
109      * android.Manifest.permission#SEND_SAFETY_CENTER_UPDATE} permission, and can use a
110      * manifest-registered receiver to be woken up by Safety Center.
111      *
112      * <p>On receiving this broadcast, safety sources should determine their safety state according
113      * to the parameters specified in the intent extras (see below) and set {@link SafetySourceData}
114      * using {@link #setSafetySourceData}, along with a {@link SafetyEvent} with {@link
115      * SafetyEvent#getType()} set to {@link SafetyEvent#SAFETY_EVENT_TYPE_REFRESH_REQUESTED} and
116      * {@link SafetyEvent#getRefreshBroadcastId()} set to the value of broadcast intent extra {@link
117      * #EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID}. If the safety source is unable to provide data,
118      * it can set a {@code null} {@link SafetySourceData}, which will clear any existing {@link
119      * SafetySourceData} stored by Safety Center, and Safety Center will fall back to any
120      * placeholder data specified in {@link SafetyCenterConfig}.
121      *
122      * <p class="note">This is a protected intent that can only be sent by the system.
123      *
124      * <p>Includes the following extras:
125      *
126      * <ul>
127      *   <li>{@link #EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE}: An int representing the type of
128      *       data being requested. Possible values are {@link
129      *       #EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA} and {@link
130      *       #EXTRA_REFRESH_REQUEST_TYPE_GET_DATA}.
131      *   <li>{@link #EXTRA_REFRESH_SAFETY_SOURCE_IDS}: A {@code String[]} of ids representing the
132      *       safety sources being requested for data. This extra exists for disambiguation in the
133      *       case that a single component is responsible for receiving refresh requests for multiple
134      *       safety sources.
135      *   <li>{@link #EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID}: An unique identifier for the
136      *       refresh request broadcast. This extra should be used to specify {@link
137      *       SafetyEvent#getRefreshBroadcastId()} when the safety source responds to the broadcast
138      *       using {@link #setSafetySourceData}.
139      * </ul>
140      */
141     @SdkConstant(BROADCAST_INTENT_ACTION)
142     public static final String ACTION_REFRESH_SAFETY_SOURCES =
143             "android.safetycenter.action.REFRESH_SAFETY_SOURCES";
144 
145     /**
146      * Used as a {@code String[]} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to
147      * specify the safety source ids of the safety sources being requested for data by Safety
148      * Center.
149      *
150      * <p>When this extra field is not specified in the intent, it is assumed that Safety Center is
151      * requesting data from all safety sources supported by the component receiving the broadcast.
152      */
153     public static final String EXTRA_REFRESH_SAFETY_SOURCE_IDS =
154             "android.safetycenter.extra.REFRESH_SAFETY_SOURCE_IDS";
155 
156     /**
157      * Used as an {@code int} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to
158      * specify the type of data request from Safety Center.
159      *
160      * <p>Possible values are {@link #EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA} and {@link
161      * #EXTRA_REFRESH_REQUEST_TYPE_GET_DATA}
162      */
163     public static final String EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE =
164             "android.safetycenter.extra.REFRESH_SAFETY_SOURCES_REQUEST_TYPE";
165 
166     /**
167      * Used as a {@code String} extra field in {@link #ACTION_REFRESH_SAFETY_SOURCES} intents to
168      * specify a string identifier for the broadcast.
169      */
170     public static final String EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID =
171             "android.safetycenter.extra.REFRESH_SAFETY_SOURCES_BROADCAST_ID";
172 
173     /**
174      * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to
175      * specify an issue ID to redirect to, if applicable.
176      *
177      * <p>This extra must be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ID} as an issue ID
178      * does not uniquely identify a {@link SafetySourceIssue}. Otherwise, no redirection will occur.
179      */
180     public static final String EXTRA_SAFETY_SOURCE_ISSUE_ID =
181             "android.safetycenter.extra.SAFETY_SOURCE_ISSUE_ID";
182 
183     /**
184      * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to
185      * specify a source ID for the {@link SafetySourceIssue} to redirect to, if applicable.
186      *
187      * <p>This extra must be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ISSUE_ID}.
188      * Otherwise, no redirection will occur.
189      */
190     public static final String EXTRA_SAFETY_SOURCE_ID =
191             "android.safetycenter.extra.SAFETY_SOURCE_ID";
192 
193     /**
194      * Used as a {@link android.os.UserHandle} extra field in {@link Intent#ACTION_SAFETY_CENTER}
195      * intents to specify a user for a given {@link SafetySourceIssue} to redirect to, if
196      * applicable.
197      *
198      * <p>This extra can be used if the same issue ID is created for multiple users (e.g. to
199      * disambiguate personal profile vs. managed profiles issues).
200      *
201      * <p>This extra can be used in conjunction with {@link #EXTRA_SAFETY_SOURCE_ISSUE_ID} and
202      * {@link #EXTRA_SAFETY_SOURCE_ID}. Otherwise, the device's primary user will be used.
203      */
204     public static final String EXTRA_SAFETY_SOURCE_USER_HANDLE =
205             "android.safetycenter.extra.SAFETY_SOURCE_USER_HANDLE";
206 
207     /**
208      * Used as a {@code String} extra field in {@link Intent#ACTION_SAFETY_CENTER} intents to
209      * specify the ID for a group of safety sources. If applicable, this will redirect to the
210      * group's corresponding subpage in the UI.
211      */
212     @RequiresApi(UPSIDE_DOWN_CAKE)
213     public static final String EXTRA_SAFETY_SOURCES_GROUP_ID =
214             "android.safetycenter.extra.SAFETY_SOURCES_GROUP_ID";
215 
216     /**
217      * Used as an int value for {@link #EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE} to indicate that
218      * the safety source should fetch fresh data relating to their safety state upon receiving a
219      * broadcast with intent action {@link #ACTION_REFRESH_SAFETY_SOURCES} and provide it to Safety
220      * Center.
221      *
222      * <p>The term "fresh" here means that the sources should ensure that the safety data is
223      * accurate as possible at the time of providing it to Safety Center, even if it involves
224      * performing an expensive and/or slow process.
225      */
226     public static final int EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA = 0;
227 
228     /**
229      * Used as an int value for {@link #EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE} to indicate that
230      * upon receiving a broadcast with intent action {@link #ACTION_REFRESH_SAFETY_SOURCES}, the
231      * safety source should provide data relating to their safety state to Safety Center.
232      *
233      * <p>If the source already has its safety data cached, it may provide it without triggering a
234      * process to fetch state which may be expensive and/or slow.
235      */
236     public static final int EXTRA_REFRESH_REQUEST_TYPE_GET_DATA = 1;
237 
238     /**
239      * All possible types of data refresh requests in broadcasts with intent action {@link
240      * #ACTION_REFRESH_SAFETY_SOURCES}.
241      *
242      * @hide
243      */
244     @IntDef(
245             prefix = {"EXTRA_REFRESH_REQUEST_TYPE_"},
246             value = {
247                 EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA,
248                 EXTRA_REFRESH_REQUEST_TYPE_GET_DATA,
249             })
250     @Retention(RetentionPolicy.SOURCE)
251     public @interface RefreshRequestType {}
252 
253     /** Indicates that the Safety Center UI has been opened by the user. */
254     public static final int REFRESH_REASON_PAGE_OPEN = 100;
255 
256     /** Indicates that the rescan button in the Safety Center UI has been clicked on by the user. */
257     public static final int REFRESH_REASON_RESCAN_BUTTON_CLICK = 200;
258 
259     /** Indicates that the device was rebooted. */
260     public static final int REFRESH_REASON_DEVICE_REBOOT = 300;
261 
262     /** Indicates that the device locale was changed. */
263     public static final int REFRESH_REASON_DEVICE_LOCALE_CHANGE = 400;
264 
265     /** Indicates that the Safety Center feature was enabled. */
266     public static final int REFRESH_REASON_SAFETY_CENTER_ENABLED = 500;
267 
268     /** Indicates a generic reason for Safety Center refresh. */
269     public static final int REFRESH_REASON_OTHER = 600;
270 
271     /** Indicates a periodic background refresh. */
272     @RequiresApi(UPSIDE_DOWN_CAKE)
273     public static final int REFRESH_REASON_PERIODIC = 700;
274 
275     /**
276      * The reason for requesting a refresh of {@link SafetySourceData} from safety sources.
277      *
278      * @hide
279      */
280     @IntDef(
281             prefix = {"REFRESH_REASON_"},
282             value = {
283                 REFRESH_REASON_PAGE_OPEN,
284                 REFRESH_REASON_RESCAN_BUTTON_CLICK,
285                 REFRESH_REASON_DEVICE_REBOOT,
286                 REFRESH_REASON_DEVICE_LOCALE_CHANGE,
287                 REFRESH_REASON_SAFETY_CENTER_ENABLED,
288                 REFRESH_REASON_OTHER,
289                 REFRESH_REASON_PERIODIC
290             })
291     @Retention(RetentionPolicy.SOURCE)
292     @TargetApi(UPSIDE_DOWN_CAKE)
293     public @interface RefreshReason {}
294 
295     /** Listener for changes to {@link SafetyCenterData}. */
296     public interface OnSafetyCenterDataChangedListener {
297 
298         /**
299          * Called when {@link SafetyCenterData} tracked by the manager changes.
300          *
301          * @param data the updated data
302          */
onSafetyCenterDataChanged(@onNull SafetyCenterData data)303         void onSafetyCenterDataChanged(@NonNull SafetyCenterData data);
304 
305         /**
306          * Called when the Safety Center should display an error related to changes in its data.
307          *
308          * @param errorDetails details of an error that should be displayed to the user
309          */
onError(@onNull SafetyCenterErrorDetails errorDetails)310         default void onError(@NonNull SafetyCenterErrorDetails errorDetails) {}
311     }
312 
313     private final Object mListenersLock = new Object();
314 
315     @GuardedBy("mListenersLock")
316     private final Map<OnSafetyCenterDataChangedListener, ListenerDelegate> mListenersToDelegates =
317             new ArrayMap<>();
318 
319     @NonNull private final Context mContext;
320     @NonNull private final ISafetyCenterManager mService;
321 
322     /**
323      * Creates a new instance of the {@link SafetyCenterManager}.
324      *
325      * @param context the {@link Context}
326      * @param service the {@link ISafetyCenterManager} service
327      * @hide
328      */
SafetyCenterManager(@onNull Context context, @NonNull ISafetyCenterManager service)329     public SafetyCenterManager(@NonNull Context context, @NonNull ISafetyCenterManager service) {
330         this.mContext = context;
331         this.mService = service;
332     }
333 
334     /**
335      * Returns whether the Safety Center feature is enabled.
336      *
337      * <p>If this returns {@code false}, all the other methods in this class will no-op and/or
338      * return default values.
339      */
340     @RequiresPermission(anyOf = {READ_SAFETY_CENTER_STATUS, SEND_SAFETY_CENTER_UPDATE})
isSafetyCenterEnabled()341     public boolean isSafetyCenterEnabled() {
342         try {
343             return mService.isSafetyCenterEnabled();
344         } catch (RemoteException e) {
345             throw e.rethrowFromSystemServer();
346         }
347     }
348 
349     /**
350      * Set the latest {@link SafetySourceData} for a safety source, to be displayed in Safety Center
351      * UI.
352      *
353      * <p>Each {@code safetySourceId} uniquely identifies the {@link SafetySourceData} for the
354      * calling user.
355      *
356      * <p>This call will rewrite any existing {@link SafetySourceData} already set for the given
357      * {@code safetySourceId} for the calling user.
358      *
359      * @param safetySourceId the unique identifier for a safety source in the calling user
360      * @param safetySourceData the latest safety data for the safety source in the calling user. If
361      *     a safety source does not have any data to set, it can set its {@link SafetySourceData} to
362      *     {@code null}, in which case Safety Center will fall back to any placeholder data
363      *     specified in the safety source xml configuration.
364      * @param safetyEvent the event that triggered the safety source to set safety data
365      */
366     @RequiresPermission(SEND_SAFETY_CENTER_UPDATE)
setSafetySourceData( @onNull String safetySourceId, @Nullable SafetySourceData safetySourceData, @NonNull SafetyEvent safetyEvent)367     public void setSafetySourceData(
368             @NonNull String safetySourceId,
369             @Nullable SafetySourceData safetySourceData,
370             @NonNull SafetyEvent safetyEvent) {
371         requireNonNull(safetySourceId, "safetySourceId cannot be null");
372         requireNonNull(safetyEvent, "safetyEvent cannot be null");
373 
374         try {
375             mService.setSafetySourceData(
376                     safetySourceId,
377                     safetySourceData,
378                     safetyEvent,
379                     mContext.getPackageName(),
380                     mContext.getUser().getIdentifier());
381         } catch (RemoteException e) {
382             throw e.rethrowFromSystemServer();
383         }
384     }
385 
386     /**
387      * Returns the latest {@link SafetySourceData} set through {@link #setSafetySourceData} for the
388      * given {@code safetySourceId} and calling user.
389      *
390      * <p>Returns {@code null} if there never was any data sent for the given {@code safetySourceId}
391      * and user.
392      */
393     @RequiresPermission(SEND_SAFETY_CENTER_UPDATE)
394     @Nullable
getSafetySourceData(@onNull String safetySourceId)395     public SafetySourceData getSafetySourceData(@NonNull String safetySourceId) {
396         requireNonNull(safetySourceId, "safetySourceId cannot be null");
397 
398         try {
399             return mService.getSafetySourceData(
400                     safetySourceId, mContext.getPackageName(), mContext.getUser().getIdentifier());
401         } catch (RemoteException e) {
402             throw e.rethrowFromSystemServer();
403         }
404     }
405 
406     /**
407      * Notifies the Safety Center of an error related to a given safety source.
408      *
409      * <p>Safety sources should use this API to notify Safety Center when Safety Center requested or
410      * expected them to perform an action or provide data, but they were unable to do so.
411      *
412      * @param safetySourceId the id of the safety source that provided the issue
413      * @param safetySourceErrorDetails details of the error that occurred
414      */
415     @RequiresPermission(SEND_SAFETY_CENTER_UPDATE)
reportSafetySourceError( @onNull String safetySourceId, @NonNull SafetySourceErrorDetails safetySourceErrorDetails)416     public void reportSafetySourceError(
417             @NonNull String safetySourceId,
418             @NonNull SafetySourceErrorDetails safetySourceErrorDetails) {
419         requireNonNull(safetySourceId, "safetySourceId cannot be null");
420         requireNonNull(safetySourceErrorDetails, "safetySourceErrorDetails cannot be null");
421 
422         try {
423             mService.reportSafetySourceError(
424                     safetySourceId,
425                     safetySourceErrorDetails,
426                     mContext.getPackageName(),
427                     mContext.getUser().getIdentifier());
428         } catch (RemoteException e) {
429             throw e.rethrowFromSystemServer();
430         }
431     }
432 
433     /**
434      * Requests safety sources to set their latest {@link SafetySourceData} for Safety Center.
435      *
436      * <p>This API sends a broadcast to all safety sources with action {@link
437      * #ACTION_REFRESH_SAFETY_SOURCES}. See {@link #ACTION_REFRESH_SAFETY_SOURCES} for details on
438      * how safety sources should respond to receiving these broadcasts.
439      *
440      * @param refreshReason the reason for the refresh
441      */
442     @RequiresPermission(MANAGE_SAFETY_CENTER)
refreshSafetySources(@efreshReason int refreshReason)443     public void refreshSafetySources(@RefreshReason int refreshReason) {
444         try {
445             mService.refreshSafetySources(refreshReason, mContext.getUser().getIdentifier());
446         } catch (RemoteException e) {
447             throw e.rethrowFromSystemServer();
448         }
449     }
450 
451     /**
452      * Requests a specific subset of safety sources to set their latest {@link SafetySourceData} for
453      * Safety Center.
454      *
455      * <p>This API sends a broadcast to safety sources with action {@link
456      * #ACTION_REFRESH_SAFETY_SOURCES} and {@link #EXTRA_REFRESH_SAFETY_SOURCE_IDS} to specify the
457      * IDs of safety sources being requested for data by Safety Center.
458      *
459      * <p>This API is an overload of {@link #refreshSafetySources(int)} and is used to request data
460      * from safety sources that are part of a subpage in the Safety Center UI.
461      *
462      * @see #refreshSafetySources(int)
463      * @param refreshReason the reason for the refresh
464      * @param safetySourceIds list of IDs for the safety sources being refreshed
465      * @throws UnsupportedOperationException if accessed from a version lower than {@link
466      *     UPSIDE_DOWN_CAKE}
467      */
468     @RequiresPermission(MANAGE_SAFETY_CENTER)
469     @RequiresApi(UPSIDE_DOWN_CAKE)
refreshSafetySources( @efreshReason int refreshReason, @NonNull List<String> safetySourceIds)470     public void refreshSafetySources(
471             @RefreshReason int refreshReason, @NonNull List<String> safetySourceIds) {
472         if (!SdkLevel.isAtLeastU()) {
473             throw new UnsupportedOperationException(
474                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
475         }
476 
477         requireNonNull(safetySourceIds, "safetySourceIds cannot be null");
478 
479         try {
480             mService.refreshSpecificSafetySources(
481                     refreshReason, mContext.getUser().getIdentifier(), safetySourceIds);
482         } catch (RemoteException e) {
483             throw e.rethrowFromSystemServer();
484         }
485     }
486 
487     /** Returns the current {@link SafetyCenterConfig}, if available. */
488     @RequiresPermission(MANAGE_SAFETY_CENTER)
489     @Nullable
getSafetyCenterConfig()490     public SafetyCenterConfig getSafetyCenterConfig() {
491         try {
492             return mService.getSafetyCenterConfig();
493         } catch (RemoteException e) {
494             throw e.rethrowFromSystemServer();
495         }
496     }
497 
498     /**
499      * Returns the current {@link SafetyCenterData}, assembled from {@link SafetySourceData} from
500      * all sources.
501      */
502     @RequiresPermission(MANAGE_SAFETY_CENTER)
503     @NonNull
getSafetyCenterData()504     public SafetyCenterData getSafetyCenterData() {
505         try {
506             return mService.getSafetyCenterData(
507                     mContext.getPackageName(), mContext.getUser().getIdentifier());
508         } catch (RemoteException e) {
509             throw e.rethrowFromSystemServer();
510         }
511     }
512 
513     /**
514      * Adds a listener for changes to {@link SafetyCenterData}.
515      *
516      * @see #removeOnSafetyCenterDataChangedListener(OnSafetyCenterDataChangedListener)
517      */
518     @RequiresPermission(MANAGE_SAFETY_CENTER)
addOnSafetyCenterDataChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnSafetyCenterDataChangedListener listener)519     public void addOnSafetyCenterDataChangedListener(
520             @NonNull @CallbackExecutor Executor executor,
521             @NonNull OnSafetyCenterDataChangedListener listener) {
522         requireNonNull(executor, "executor cannot be null");
523         requireNonNull(listener, "listener cannot be null");
524 
525         synchronized (mListenersLock) {
526             if (mListenersToDelegates.containsKey(listener)) return;
527 
528             ListenerDelegate delegate = new ListenerDelegate(executor, listener);
529             try {
530                 mService.addOnSafetyCenterDataChangedListener(
531                         delegate, mContext.getPackageName(), mContext.getUser().getIdentifier());
532             } catch (RemoteException e) {
533                 throw e.rethrowFromSystemServer();
534             }
535             mListenersToDelegates.put(listener, delegate);
536         }
537     }
538 
539     /**
540      * Removes a listener for changes to {@link SafetyCenterData}.
541      *
542      * @see #addOnSafetyCenterDataChangedListener(Executor, OnSafetyCenterDataChangedListener)
543      */
544     @RequiresPermission(MANAGE_SAFETY_CENTER)
removeOnSafetyCenterDataChangedListener( @onNull OnSafetyCenterDataChangedListener listener)545     public void removeOnSafetyCenterDataChangedListener(
546             @NonNull OnSafetyCenterDataChangedListener listener) {
547         requireNonNull(listener, "listener cannot be null");
548 
549         synchronized (mListenersLock) {
550             ListenerDelegate delegate = mListenersToDelegates.get(listener);
551             if (delegate == null) return;
552 
553             try {
554                 mService.removeOnSafetyCenterDataChangedListener(
555                         delegate, mContext.getUser().getIdentifier());
556             } catch (RemoteException e) {
557                 throw e.rethrowFromSystemServer();
558             }
559             delegate.markAsRemoved();
560             mListenersToDelegates.remove(listener);
561         }
562     }
563 
564     /**
565      * Dismiss a Safety Center issue and prevent it from affecting the overall safety status.
566      *
567      * @param safetyCenterIssueId the target issue ID returned by {@link SafetyCenterIssue#getId()}
568      */
569     @RequiresPermission(MANAGE_SAFETY_CENTER)
dismissSafetyCenterIssue(@onNull String safetyCenterIssueId)570     public void dismissSafetyCenterIssue(@NonNull String safetyCenterIssueId) {
571         requireNonNull(safetyCenterIssueId, "safetyCenterIssueId cannot be null");
572 
573         try {
574             mService.dismissSafetyCenterIssue(
575                     safetyCenterIssueId, mContext.getUser().getIdentifier());
576         } catch (RemoteException e) {
577             throw e.rethrowFromSystemServer();
578         }
579     }
580 
581     /**
582      * Executes the specified Safety Center issue action on the specified Safety Center issue.
583      *
584      * @param safetyCenterIssueId the target issue ID returned by {@link SafetyCenterIssue#getId()}
585      * @param safetyCenterIssueActionId the target action ID returned by {@link
586      *     SafetyCenterIssue.Action#getId()}
587      */
588     @RequiresPermission(MANAGE_SAFETY_CENTER)
executeSafetyCenterIssueAction( @onNull String safetyCenterIssueId, @NonNull String safetyCenterIssueActionId)589     public void executeSafetyCenterIssueAction(
590             @NonNull String safetyCenterIssueId, @NonNull String safetyCenterIssueActionId) {
591         requireNonNull(safetyCenterIssueId, "safetyCenterIssueId cannot be null");
592         requireNonNull(safetyCenterIssueActionId, "safetyCenterIssueActionId cannot be null");
593 
594         try {
595             mService.executeSafetyCenterIssueAction(
596                     safetyCenterIssueId,
597                     safetyCenterIssueActionId,
598                     mContext.getUser().getIdentifier());
599         } catch (RemoteException e) {
600             throw e.rethrowFromSystemServer();
601         }
602     }
603 
604     /**
605      * Clears all {@link SafetySourceData} (set by safety sources using {@link
606      * #setSafetySourceData}) for testing.
607      *
608      * <p>Note: This API serves to facilitate CTS testing and should not be used for other purposes.
609      */
610     @RequiresPermission(MANAGE_SAFETY_CENTER)
clearAllSafetySourceDataForTests()611     public void clearAllSafetySourceDataForTests() {
612         try {
613             mService.clearAllSafetySourceDataForTests();
614         } catch (RemoteException e) {
615             throw e.rethrowFromSystemServer();
616         }
617     }
618 
619     /**
620      * Overrides the {@link SafetyCenterConfig} for testing.
621      *
622      * <p>When set, the overridden {@link SafetyCenterConfig} will be used instead of the {@link
623      * SafetyCenterConfig} parsed from the XML file to read configured safety sources.
624      *
625      * <p>Note: This API serves to facilitate CTS testing and should not be used to configure safety
626      * sources dynamically for production. Once used for testing, the override should be cleared.
627      *
628      * @see #clearSafetyCenterConfigForTests()
629      */
630     @RequiresPermission(MANAGE_SAFETY_CENTER)
setSafetyCenterConfigForTests(@onNull SafetyCenterConfig safetyCenterConfig)631     public void setSafetyCenterConfigForTests(@NonNull SafetyCenterConfig safetyCenterConfig) {
632         requireNonNull(safetyCenterConfig, "safetyCenterConfig cannot be null");
633 
634         try {
635             mService.setSafetyCenterConfigForTests(safetyCenterConfig);
636         } catch (RemoteException e) {
637             throw e.rethrowFromSystemServer();
638         }
639     }
640 
641     /**
642      * Clears the override of the {@link SafetyCenterConfig} set for testing.
643      *
644      * <p>Once cleared, the {@link SafetyCenterConfig} parsed from the XML file will be used to read
645      * configured safety sources.
646      *
647      * <p>Note: This API serves to facilitate CTS testing and should not be used for other purposes.
648      *
649      * @see #setSafetyCenterConfigForTests(SafetyCenterConfig)
650      */
651     @RequiresPermission(MANAGE_SAFETY_CENTER)
clearSafetyCenterConfigForTests()652     public void clearSafetyCenterConfigForTests() {
653         try {
654             mService.clearSafetyCenterConfigForTests();
655         } catch (RemoteException e) {
656             throw e.rethrowFromSystemServer();
657         }
658     }
659 
660     private static final class ListenerDelegate extends IOnSafetyCenterDataChangedListener.Stub {
661         @NonNull private final Executor mExecutor;
662         @NonNull private final OnSafetyCenterDataChangedListener mOriginalListener;
663 
664         private volatile boolean mRemoved = false;
665 
ListenerDelegate( @onNull Executor executor, @NonNull OnSafetyCenterDataChangedListener originalListener)666         private ListenerDelegate(
667                 @NonNull Executor executor,
668                 @NonNull OnSafetyCenterDataChangedListener originalListener) {
669             mExecutor = executor;
670             mOriginalListener = originalListener;
671         }
672 
673         @Override
onSafetyCenterDataChanged(@onNull SafetyCenterData safetyCenterData)674         public void onSafetyCenterDataChanged(@NonNull SafetyCenterData safetyCenterData) {
675             requireNonNull(safetyCenterData, "safetyCenterData cannot be null");
676 
677             final long identity = Binder.clearCallingIdentity();
678             try {
679                 mExecutor.execute(
680                         () -> {
681                             if (mRemoved) {
682                                 return;
683                             }
684                             // The remove call could still complete at this point, but we're ok
685                             // with this raciness on separate threads. If the listener is removed on
686                             // the same thread as `mExecutor`; the above check should ensure that
687                             // the listener won't be called after the remove call.
688                             mOriginalListener.onSafetyCenterDataChanged(safetyCenterData);
689                         });
690             } finally {
691                 Binder.restoreCallingIdentity(identity);
692             }
693         }
694 
695         @Override
onError(@onNull SafetyCenterErrorDetails safetyCenterErrorDetails)696         public void onError(@NonNull SafetyCenterErrorDetails safetyCenterErrorDetails) {
697             requireNonNull(safetyCenterErrorDetails, "safetyCenterErrorDetails cannot be null");
698 
699             final long identity = Binder.clearCallingIdentity();
700             try {
701                 mExecutor.execute(
702                         () -> {
703                             if (mRemoved) {
704                                 return;
705                             }
706                             // The remove call could still complete at this point, but we're ok
707                             // with this raciness on separate threads. If the listener is removed on
708                             // the same thread as `mExecutor`; the above check should ensure that
709                             // the listener won't be called after the remove call.
710                             mOriginalListener.onError(safetyCenterErrorDetails);
711                         });
712             } finally {
713                 Binder.restoreCallingIdentity(identity);
714             }
715         }
716 
markAsRemoved()717         public void markAsRemoved() {
718             mRemoved = true;
719         }
720     }
721 }
722