1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.safetycenter.data;
18 
19 import static android.Manifest.permission.MANAGE_SAFETY_CENTER;
20 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
21 
22 import static com.android.safetycenter.logging.SafetyCenterStatsdLogger.toSystemEventResult;
23 
24 import android.annotation.UserIdInt;
25 import android.content.Context;
26 import android.safetycenter.SafetyCenterData;
27 import android.safetycenter.SafetyEvent;
28 import android.safetycenter.SafetySourceData;
29 import android.safetycenter.SafetySourceErrorDetails;
30 import android.safetycenter.SafetySourceIssue;
31 import android.safetycenter.config.SafetyCenterConfig;
32 import android.safetycenter.config.SafetySourcesGroup;
33 import android.util.ArraySet;
34 import android.util.Log;
35 
36 import androidx.annotation.Nullable;
37 
38 import com.android.safetycenter.ApiLock;
39 import com.android.safetycenter.SafetyCenterConfigReader;
40 import com.android.safetycenter.SafetyCenterRefreshTracker;
41 import com.android.safetycenter.SafetySourceIssueInfo;
42 import com.android.safetycenter.SafetySourceKey;
43 import com.android.safetycenter.UserProfileGroup;
44 import com.android.safetycenter.UserProfileGroup.ProfileType;
45 import com.android.safetycenter.internaldata.SafetyCenterIssueActionId;
46 import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
47 import com.android.safetycenter.logging.SafetyCenterStatsdLogger;
48 
49 import java.io.FileDescriptor;
50 import java.io.PrintWriter;
51 import java.time.Instant;
52 import java.util.List;
53 import java.util.Set;
54 
55 import javax.annotation.concurrent.NotThreadSafe;
56 
57 /**
58  * Manages updates and access to all data in the data subpackage.
59  *
60  * <p>Data entails what safety sources reported to safety center, including issues, entries,
61  * dismissals, errors, in-flight actions, etc.
62  *
63  * @hide
64  */
65 @NotThreadSafe
66 public final class SafetyCenterDataManager {
67 
68     private static final String TAG = "SafetyCenterDataManager";
69 
70     private final Context mContext;
71     private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker;
72     private final SafetySourceDataRepository mSafetySourceDataRepository;
73     private final SafetyCenterIssueDismissalRepository mSafetyCenterIssueDismissalRepository;
74     private final SafetyCenterIssueRepository mSafetyCenterIssueRepository;
75     private final SafetyCenterInFlightIssueActionRepository
76             mSafetyCenterInFlightIssueActionRepository;
77     private final SafetySourceDataValidator mSafetySourceDataValidator;
78     private final SafetySourceStateCollectedLogger mSafetySourceStateCollectedLogger;
79 
80     /** Creates an instance of {@link SafetyCenterDataManager}. */
SafetyCenterDataManager( Context context, SafetyCenterConfigReader safetyCenterConfigReader, SafetyCenterRefreshTracker safetyCenterRefreshTracker, ApiLock apiLock)81     public SafetyCenterDataManager(
82             Context context,
83             SafetyCenterConfigReader safetyCenterConfigReader,
84             SafetyCenterRefreshTracker safetyCenterRefreshTracker,
85             ApiLock apiLock) {
86         mContext = context;
87         mSafetyCenterRefreshTracker = safetyCenterRefreshTracker;
88         mSafetyCenterInFlightIssueActionRepository =
89                 new SafetyCenterInFlightIssueActionRepository(context);
90         mSafetyCenterIssueDismissalRepository =
91                 new SafetyCenterIssueDismissalRepository(apiLock, safetyCenterConfigReader);
92         mSafetySourceDataRepository =
93                 new SafetySourceDataRepository(
94                         mSafetyCenterInFlightIssueActionRepository,
95                         mSafetyCenterIssueDismissalRepository);
96         mSafetyCenterIssueRepository =
97                 new SafetyCenterIssueRepository(
98                         context,
99                         mSafetySourceDataRepository,
100                         safetyCenterConfigReader,
101                         mSafetyCenterIssueDismissalRepository,
102                         new SafetyCenterIssueDeduplicator(mSafetyCenterIssueDismissalRepository));
103         mSafetySourceDataValidator =
104                 new SafetySourceDataValidator(context, safetyCenterConfigReader);
105         mSafetySourceStateCollectedLogger =
106                 new SafetySourceStateCollectedLogger(
107                         context,
108                         mSafetySourceDataRepository,
109                         mSafetyCenterIssueDismissalRepository,
110                         mSafetyCenterIssueRepository);
111     }
112 
113     ///////////////////////////////////////////////////////////////////////////////////////////////
114     //////////////////////   STATE UPDATES ////////////////////////////////////////////////////////
115     ///////////////////////////////////////////////////////////////////////////////////////////////
116 
117     /**
118      * Sets the latest {@link SafetySourceData} for the given {@code safetySourceId}, {@link
119      * SafetyEvent}, {@code packageName} and {@code userId}, and returns {@code true} if this caused
120      * any changes which would alter {@link SafetyCenterData}.
121      *
122      * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code
123      * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected; or the {@link
124      * SafetySourceData} does not respect all constraints defined in the config.
125      *
126      * <p>Setting a {@code null} {@link SafetySourceData} evicts the current {@link
127      * SafetySourceData} entry and clears the {@link SafetyCenterIssueDismissalRepository} for the
128      * source.
129      *
130      * <p>This method may modify the {@link SafetyCenterIssueDismissalRepository}.
131      */
setSafetySourceData( @ullable SafetySourceData safetySourceData, String safetySourceId, SafetyEvent safetyEvent, String packageName, @UserIdInt int userId)132     public boolean setSafetySourceData(
133             @Nullable SafetySourceData safetySourceData,
134             String safetySourceId,
135             SafetyEvent safetyEvent,
136             String packageName,
137             @UserIdInt int userId) {
138         if (!mSafetySourceDataValidator.validateRequest(
139                 safetySourceData,
140                 /* callerCanAccessAnySource= */ false,
141                 safetySourceId,
142                 packageName,
143                 userId)) {
144             return false;
145         }
146         SafetySourceKey safetySourceKey = SafetySourceKey.of(safetySourceId, userId);
147 
148         // Must fetch refresh reason before calling processSafetyEvent because the latter may
149         // complete and clear the current refresh.
150         // TODO(b/277174417): Restructure this code to avoid this error-prone sequencing concern
151         Integer refreshReason = null;
152         if (safetyEvent.getType() == SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED) {
153             refreshReason = mSafetyCenterRefreshTracker.getRefreshReason();
154         }
155 
156         // It is important to process the event first as it relies on the data available prior to
157         // changing it.
158         boolean sourceDataWillChange =
159                 !mSafetySourceDataRepository.sourceHasData(safetySourceKey, safetySourceData);
160         boolean eventCausedChange =
161                 processSafetyEvent(
162                         safetySourceKey, safetyEvent, /* isError= */ false, sourceDataWillChange);
163         boolean sourceDataDiffers =
164                 mSafetySourceDataRepository.setSafetySourceData(safetySourceKey, safetySourceData);
165         boolean safetyCenterDataChanged = sourceDataDiffers || eventCausedChange;
166 
167         if (safetyCenterDataChanged) {
168             mSafetyCenterIssueRepository.updateIssues(userId);
169         }
170 
171         mSafetySourceStateCollectedLogger.writeSourceUpdatedAtom(
172                 safetySourceKey,
173                 safetySourceData,
174                 refreshReason,
175                 sourceDataDiffers,
176                 userId,
177                 safetyEvent);
178 
179         return safetyCenterDataChanged;
180     }
181 
182     /**
183      * Marks the issue with the given key as dismissed.
184      *
185      * <p>That issue's notification (if any) is also marked as dismissed.
186      */
dismissSafetyCenterIssue(SafetyCenterIssueKey safetyCenterIssueKey)187     public void dismissSafetyCenterIssue(SafetyCenterIssueKey safetyCenterIssueKey) {
188         mSafetyCenterIssueDismissalRepository.dismissIssue(safetyCenterIssueKey);
189         mSafetyCenterIssueRepository.updateIssues(safetyCenterIssueKey.getUserId());
190     }
191 
192     /**
193      * Marks the notification (if any) of the issue with the given key as dismissed.
194      *
195      * <p>The issue itself is <strong>not</strong> marked as dismissed and its warning card can
196      * still appear in the Safety Center UI.
197      */
dismissNotification(SafetyCenterIssueKey safetyCenterIssueKey)198     public void dismissNotification(SafetyCenterIssueKey safetyCenterIssueKey) {
199         mSafetyCenterIssueDismissalRepository.dismissNotification(safetyCenterIssueKey);
200         mSafetyCenterIssueRepository.updateIssues(safetyCenterIssueKey.getUserId());
201     }
202 
203     /**
204      * Reports the given {@link SafetySourceErrorDetails} for the given {@code safetySourceId} and
205      * {@code userId}, and returns whether there was a change to the underlying {@link
206      * SafetyCenterData}.
207      *
208      * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code
209      * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected.
210      */
reportSafetySourceError( SafetySourceErrorDetails safetySourceErrorDetails, String safetySourceId, String packageName, @UserIdInt int userId)211     public boolean reportSafetySourceError(
212             SafetySourceErrorDetails safetySourceErrorDetails,
213             String safetySourceId,
214             String packageName,
215             @UserIdInt int userId) {
216         if (!mSafetySourceDataValidator.validateRequest(
217                 /* safetySourceData= */ null,
218                 /* callerCanAccessAnySource= */ false,
219                 safetySourceId,
220                 packageName,
221                 userId)) {
222             return false;
223         }
224         SafetyEvent safetyEvent = safetySourceErrorDetails.getSafetyEvent();
225         SafetySourceKey safetySourceKey = SafetySourceKey.of(safetySourceId, userId);
226 
227         // Must fetch refresh reason before calling processSafetyEvent because the latter may
228         // complete and clear the current refresh.
229         // TODO(b/277174417): Restructure this code to avoid this error-prone sequencing concern
230         Integer refreshReason = null;
231         if (safetyEvent.getType() == SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED) {
232             refreshReason = mSafetyCenterRefreshTracker.getRefreshReason();
233         }
234 
235         // It is important to process the event first as it relies on the data available prior to
236         // changing it.
237         boolean sourceDataWillChange = !mSafetySourceDataRepository.sourceHasError(safetySourceKey);
238         boolean eventCausedChange =
239                 processSafetyEvent(
240                         safetySourceKey, safetyEvent, /* isError= */ true, sourceDataWillChange);
241         boolean sourceDataDiffers =
242                 mSafetySourceDataRepository.reportSafetySourceError(
243                         safetySourceKey, safetySourceErrorDetails);
244         boolean safetyCenterDataChanged = sourceDataDiffers || eventCausedChange;
245 
246         if (safetyCenterDataChanged) {
247             mSafetyCenterIssueRepository.updateIssues(userId);
248         }
249 
250         mSafetySourceStateCollectedLogger.writeSourceUpdatedAtom(
251                 safetySourceKey,
252                 /* safetySourceData= */ null,
253                 refreshReason,
254                 sourceDataDiffers,
255                 userId,
256                 safetyEvent);
257 
258         return safetyCenterDataChanged;
259     }
260 
261     /**
262      * Marks the given {@link SafetySourceKey} as having timed out during a refresh.
263      *
264      * @param setError whether we should clear the data associated with the source and set an error
265      */
markSafetySourceRefreshTimedOut(SafetySourceKey safetySourceKey, boolean setError)266     public void markSafetySourceRefreshTimedOut(SafetySourceKey safetySourceKey, boolean setError) {
267         boolean dataUpdated =
268                 mSafetySourceDataRepository.markSafetySourceRefreshTimedOut(
269                         safetySourceKey, setError);
270         if (dataUpdated) {
271             mSafetyCenterIssueRepository.updateIssues(safetySourceKey.getUserId());
272         }
273     }
274 
275     /** Marks the given {@link SafetyCenterIssueActionId} as in-flight. */
markSafetyCenterIssueActionInFlight( SafetyCenterIssueActionId safetyCenterIssueActionId)276     public void markSafetyCenterIssueActionInFlight(
277             SafetyCenterIssueActionId safetyCenterIssueActionId) {
278         mSafetyCenterInFlightIssueActionRepository.markSafetyCenterIssueActionInFlight(
279                 safetyCenterIssueActionId);
280         mSafetyCenterIssueRepository.updateIssues(
281                 safetyCenterIssueActionId.getSafetyCenterIssueKey().getUserId());
282     }
283 
284     /**
285      * Unmarks the given {@link SafetyCenterIssueActionId} as in-flight and returns {@code true} if
286      * this caused any changes which would alter {@link SafetyCenterData}.
287      *
288      * <p>Also logs an event to statsd with the given {@code result} value.
289      */
unmarkSafetyCenterIssueActionInFlight( SafetyCenterIssueActionId safetyCenterIssueActionId, @Nullable SafetySourceIssue safetySourceIssue, @SafetyCenterStatsdLogger.SystemEventResult int result)290     public boolean unmarkSafetyCenterIssueActionInFlight(
291             SafetyCenterIssueActionId safetyCenterIssueActionId,
292             @Nullable SafetySourceIssue safetySourceIssue,
293             @SafetyCenterStatsdLogger.SystemEventResult int result) {
294         boolean dataUpdated =
295                 mSafetyCenterInFlightIssueActionRepository.unmarkSafetyCenterIssueActionInFlight(
296                         safetyCenterIssueActionId, safetySourceIssue, result);
297         if (dataUpdated) {
298             mSafetyCenterIssueRepository.updateIssues(
299                     safetyCenterIssueActionId.getSafetyCenterIssueKey().getUserId());
300         }
301         return dataUpdated;
302     }
303 
304     /** Clears all data related to the given {@code userId}. */
clearForUser(@serIdInt int userId)305     public void clearForUser(@UserIdInt int userId) {
306         mSafetySourceDataRepository.clearForUser(userId);
307         mSafetyCenterInFlightIssueActionRepository.clearForUser(userId);
308         mSafetyCenterIssueDismissalRepository.clearForUser(userId);
309         mSafetyCenterIssueRepository.clearForUser(userId);
310     }
311 
312     /** Clears all stored data. */
clear()313     public void clear() {
314         mSafetySourceDataRepository.clear();
315         mSafetyCenterIssueDismissalRepository.clear();
316         mSafetyCenterInFlightIssueActionRepository.clear();
317         mSafetyCenterIssueRepository.clear();
318     }
319 
320     ///////////////////////////////////////////////////////////////////////////////////////////////
321     //////////////////////   SafetyCenterIssueDismissalRepository /////////////////////////////////
322     ///////////////////////////////////////////////////////////////////////////////////////////////
323 
324     /**
325      * Returns {@code true} if the issue with the given key and severity level is currently
326      * dismissed.
327      *
328      * <p>An issue which is dismissed at one time may become "un-dismissed" later, after the
329      * resurface delay (which depends on severity level) has elapsed.
330      *
331      * <p>If the given issue key is not found in the repository this method returns {@code false}.
332      */
isIssueDismissed( SafetyCenterIssueKey safetyCenterIssueKey, @SafetySourceData.SeverityLevel int safetySourceIssueSeverityLevel)333     public boolean isIssueDismissed(
334             SafetyCenterIssueKey safetyCenterIssueKey,
335             @SafetySourceData.SeverityLevel int safetySourceIssueSeverityLevel) {
336         return mSafetyCenterIssueDismissalRepository.isIssueDismissed(
337                 safetyCenterIssueKey, safetySourceIssueSeverityLevel);
338     }
339 
340     /** Returns {@code true} if an issue's notification is dismissed now. */
341     // TODO(b/259084807): Consider extracting notification dismissal logic to separate class
isNotificationDismissedNow( SafetyCenterIssueKey issueKey, @SafetySourceData.SeverityLevel int severityLevel)342     public boolean isNotificationDismissedNow(
343             SafetyCenterIssueKey issueKey, @SafetySourceData.SeverityLevel int severityLevel) {
344         return mSafetyCenterIssueDismissalRepository.isNotificationDismissedNow(
345                 issueKey, severityLevel);
346     }
347 
348     /**
349      * Load available persisted data state into memory.
350      *
351      * <p>Note: only some pieces of the data can be persisted, the rest won't be loaded.
352      */
loadPersistableDataStateFromFile()353     public void loadPersistableDataStateFromFile() {
354         mSafetyCenterIssueDismissalRepository.loadStateFromFile();
355     }
356 
357     /**
358      * Returns the {@link Instant} when the issue with the given key was first reported to Safety
359      * Center.
360      */
361     @Nullable
getIssueFirstSeenAt(SafetyCenterIssueKey safetyCenterIssueKey)362     public Instant getIssueFirstSeenAt(SafetyCenterIssueKey safetyCenterIssueKey) {
363         return mSafetyCenterIssueDismissalRepository.getIssueFirstSeenAt(safetyCenterIssueKey);
364     }
365 
366     ///////////////////////////////////////////////////////////////////////////////////////////////
367     //////////////////////   SafetyCenterIssueRepository  /////////////////////////////////////////
368     ///////////////////////////////////////////////////////////////////////////////////////////////
369 
370     /**
371      * Fetches a list of issues related to the given {@link UserProfileGroup}.
372      *
373      * <p>Issues in the list are sorted in descending order and deduplicated (if applicable, only on
374      * Android U+).
375      *
376      * <p>Only includes issues related to active/running {@code userId}s in the given {@link
377      * UserProfileGroup}.
378      */
getIssuesDedupedSortedDescFor( UserProfileGroup userProfileGroup)379     public List<SafetySourceIssueInfo> getIssuesDedupedSortedDescFor(
380             UserProfileGroup userProfileGroup) {
381         return mSafetyCenterIssueRepository.getIssuesDedupedSortedDescFor(userProfileGroup);
382     }
383 
384     /**
385      * Counts the total number of issues from loggable sources, in the given {@link
386      * UserProfileGroup}.
387      *
388      * <p>Only includes issues related to active/running {@code userId}s in the given {@link
389      * UserProfileGroup}.
390      */
countLoggableIssuesFor(UserProfileGroup userProfileGroup)391     public int countLoggableIssuesFor(UserProfileGroup userProfileGroup) {
392         return mSafetyCenterIssueRepository.countLoggableIssuesFor(userProfileGroup);
393     }
394 
395     /** Gets an unmodifiable list of all issues for the given {@code userId}. */
getIssuesForUser(@serIdInt int userId)396     public List<SafetySourceIssueInfo> getIssuesForUser(@UserIdInt int userId) {
397         return mSafetyCenterIssueRepository.getIssuesForUser(userId);
398     }
399 
400     /**
401      * Returns a set of {@link SafetySourcesGroup} IDs that the given {@link SafetyCenterIssueKey}
402      * is mapped to, or an empty list of no such mapping is configured.
403      *
404      * <p>Issue being mapped to a group means that this issue is relevant to that group.
405      */
getGroupMappingFor(SafetyCenterIssueKey issueKey)406     public Set<String> getGroupMappingFor(SafetyCenterIssueKey issueKey) {
407         return mSafetyCenterIssueRepository.getGroupMappingFor(issueKey);
408     }
409 
410     ///////////////////////////////////////////////////////////////////////////////////////////////
411     //////////////////////   SafetyCenterInFlightIssueActionRepository ////////////////////////////
412     ///////////////////////////////////////////////////////////////////////////////////////////////
413 
414     /** Returns {@code true} if the given issue action is in flight. */
actionIsInFlight(SafetyCenterIssueActionId safetyCenterIssueActionId)415     public boolean actionIsInFlight(SafetyCenterIssueActionId safetyCenterIssueActionId) {
416         return mSafetyCenterInFlightIssueActionRepository.actionIsInFlight(
417                 safetyCenterIssueActionId);
418     }
419 
420     /** Returns a list of IDs of in-flight actions for the given source and user */
getInFlightActions(String sourceId, @UserIdInt int userId)421     ArraySet<SafetyCenterIssueActionId> getInFlightActions(String sourceId, @UserIdInt int userId) {
422         return mSafetyCenterInFlightIssueActionRepository.getInFlightActions(sourceId, userId);
423     }
424 
425     ///////////////////////////////////////////////////////////////////////////////////////////////
426     //////////////////////   SafetySourceDataRepository ///////////////////////////////////////////
427     ///////////////////////////////////////////////////////////////////////////////////////////////
428 
429     /**
430      * Returns the latest {@link SafetySourceData} that was set by {@link #setSafetySourceData} for
431      * the given {@code safetySourceId}, {@code packageName} and {@code userId}.
432      *
433      * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code
434      * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected.
435      *
436      * <p>Returns {@code null} if it was never set since boot, or if the entry was evicted using
437      * {@link #setSafetySourceData} with a {@code null} value.
438      */
439     @Nullable
getSafetySourceData( String safetySourceId, String packageName, @UserIdInt int userId)440     public SafetySourceData getSafetySourceData(
441             String safetySourceId, String packageName, @UserIdInt int userId) {
442         boolean callerCanAccessAnySource =
443                 mContext.checkCallingOrSelfPermission(MANAGE_SAFETY_CENTER) == PERMISSION_GRANTED;
444         if (!mSafetySourceDataValidator.validateRequest(
445                 /* safetySourceData= */ null,
446                 callerCanAccessAnySource,
447                 safetySourceId,
448                 packageName,
449                 userId)) {
450             return null;
451         }
452         return mSafetySourceDataRepository.getSafetySourceData(
453                 SafetySourceKey.of(safetySourceId, userId));
454     }
455 
456     /**
457      * Returns the latest {@link SafetySourceData} that was set by {@link #setSafetySourceData} for
458      * the given {@link SafetySourceKey}.
459      *
460      * <p>This method does not perform any validation, {@link #getSafetySourceData(String, String,
461      * int)} should be called wherever validation is required.
462      *
463      * <p>Returns {@code null} if it was never set since boot, or if the entry was evicted using
464      * {@link #setSafetySourceData} with a {@code null} value.
465      */
466     @Nullable
getSafetySourceDataInternal(SafetySourceKey safetySourceKey)467     public SafetySourceData getSafetySourceDataInternal(SafetySourceKey safetySourceKey) {
468         return mSafetySourceDataRepository.getSafetySourceData(safetySourceKey);
469     }
470 
471     /** Returns {@code true} if the given source has an error. */
sourceHasError(SafetySourceKey safetySourceKey)472     public boolean sourceHasError(SafetySourceKey safetySourceKey) {
473         return mSafetySourceDataRepository.sourceHasError(safetySourceKey);
474     }
475 
476     /**
477      * Returns the {@link SafetySourceIssue} associated with the given {@link SafetyCenterIssueKey}.
478      *
479      * <p>Returns {@code null} if there is no such {@link SafetySourceIssue}.
480      */
481     @Nullable
getSafetySourceIssue(SafetyCenterIssueKey safetyCenterIssueKey)482     public SafetySourceIssue getSafetySourceIssue(SafetyCenterIssueKey safetyCenterIssueKey) {
483         return mSafetySourceDataRepository.getSafetySourceIssue(safetyCenterIssueKey);
484     }
485 
486     /**
487      * Returns the {@link SafetySourceIssue.Action} associated with the given {@link
488      * SafetyCenterIssueActionId}.
489      *
490      * <p>Returns {@code null} if there is no associated {@link SafetySourceIssue}, or if it's been
491      * dismissed.
492      *
493      * <p>Returns {@code null} if the {@link SafetySourceIssue.Action} is currently in flight.
494      */
495     @Nullable
getSafetySourceIssueAction( SafetyCenterIssueActionId safetyCenterIssueActionId)496     public SafetySourceIssue.Action getSafetySourceIssueAction(
497             SafetyCenterIssueActionId safetyCenterIssueActionId) {
498         return mSafetySourceDataRepository.getSafetySourceIssueAction(safetyCenterIssueActionId);
499     }
500 
501     ///////////////////////////////////////////////////////////////////////////////////////////////
502     ////////////////////////////  Other   /////////////////////////////////////////////////////////
503     ///////////////////////////////////////////////////////////////////////////////////////////////
504 
505     /** Dumps state for debugging purposes. */
dump(FileDescriptor fd, PrintWriter fout)506     public void dump(FileDescriptor fd, PrintWriter fout) {
507         mSafetySourceDataRepository.dump(fout);
508         mSafetyCenterIssueDismissalRepository.dump(fd, fout);
509         mSafetyCenterInFlightIssueActionRepository.dump(fout);
510         mSafetyCenterIssueRepository.dump(fout);
511     }
512 
processSafetyEvent( SafetySourceKey safetySourceKey, SafetyEvent safetyEvent, boolean isError, boolean sourceDataWillChange)513     private boolean processSafetyEvent(
514             SafetySourceKey safetySourceKey,
515             SafetyEvent safetyEvent,
516             boolean isError,
517             boolean sourceDataWillChange) {
518         int type = safetyEvent.getType();
519         switch (type) {
520             case SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED:
521                 String refreshBroadcastId = safetyEvent.getRefreshBroadcastId();
522                 if (refreshBroadcastId == null) {
523                     Log.w(TAG, "No refresh broadcast id in SafetyEvent of type: " + type);
524                     return false;
525                 }
526                 return mSafetyCenterRefreshTracker.reportSourceRefreshCompleted(
527                         refreshBroadcastId, safetySourceKey, !isError, sourceDataWillChange);
528             case SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED:
529             case SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED:
530                 String safetySourceIssueId = safetyEvent.getSafetySourceIssueId();
531                 if (safetySourceIssueId == null) {
532                     Log.w(TAG, "No safety source issue id in SafetyEvent of type: " + type);
533                     return false;
534                 }
535                 String safetySourceIssueActionId = safetyEvent.getSafetySourceIssueActionId();
536                 if (safetySourceIssueActionId == null) {
537                     Log.w(TAG, "No safety source issue action id in SafetyEvent of type: " + type);
538                     return false;
539                 }
540                 SafetyCenterIssueKey safetyCenterIssueKey =
541                         SafetyCenterIssueKey.newBuilder()
542                                 .setSafetySourceId(safetySourceKey.getSourceId())
543                                 .setSafetySourceIssueId(safetySourceIssueId)
544                                 .setUserId(safetySourceKey.getUserId())
545                                 .build();
546                 SafetyCenterIssueActionId safetyCenterIssueActionId =
547                         SafetyCenterIssueActionId.newBuilder()
548                                 .setSafetyCenterIssueKey(safetyCenterIssueKey)
549                                 .setSafetySourceIssueActionId(safetySourceIssueActionId)
550                                 .build();
551                 boolean success = type == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED;
552                 int result = toSystemEventResult(success);
553                 return mSafetyCenterInFlightIssueActionRepository
554                         .unmarkSafetyCenterIssueActionInFlight(
555                                 safetyCenterIssueActionId,
556                                 getSafetySourceIssue(safetyCenterIssueKey),
557                                 result);
558             case SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED:
559             case SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_LOCALE_CHANGED:
560             case SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_REBOOTED:
561                 return false;
562         }
563         Log.w(TAG, "Unexpected SafetyEvent.Type: " + type);
564         return false;
565     }
566 
567     /**
568      * Writes a SafetySourceStateCollected atom for the given source in response to a stats pull.
569      */
logSafetySourceStateCollectedAutomatic( SafetySourceKey sourceKey, @ProfileType int profileType)570     public void logSafetySourceStateCollectedAutomatic(
571             SafetySourceKey sourceKey, @ProfileType int profileType) {
572         mSafetySourceStateCollectedLogger.writeAutomaticAtom(sourceKey, profileType);
573     }
574 }
575