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 com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__DATA_PROVIDED;
20 import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__NO_DATA_PROVIDED;
21 import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__REFRESH_TIMEOUT;
22 import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_CLEARED;
23 import static com.android.permission.PermissionStatsLog.SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_ERROR;
24 
25 import android.annotation.UptimeMillisLong;
26 import android.annotation.UserIdInt;
27 import android.os.SystemClock;
28 import android.safetycenter.SafetyCenterData;
29 import android.safetycenter.SafetyEvent;
30 import android.safetycenter.SafetySourceData;
31 import android.safetycenter.SafetySourceErrorDetails;
32 import android.safetycenter.SafetySourceIssue;
33 import android.util.ArrayMap;
34 import android.util.ArraySet;
35 import android.util.Log;
36 
37 import androidx.annotation.Nullable;
38 
39 import com.android.safetycenter.SafetySourceKey;
40 import com.android.safetycenter.internaldata.SafetyCenterIssueActionId;
41 import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
42 import com.android.safetycenter.logging.SafetyCenterStatsdLogger;
43 
44 import java.io.PrintWriter;
45 import java.util.List;
46 import java.util.Objects;
47 
48 import javax.annotation.concurrent.NotThreadSafe;
49 
50 /**
51  * Repository for {@link SafetySourceData} and other data managed by Safety Center including {@link
52  * SafetySourceErrorDetails}.
53  *
54  * <p>This class isn't thread safe. Thread safety must be handled by the caller.
55  */
56 @NotThreadSafe
57 final class SafetySourceDataRepository {
58 
59     private static final String TAG = "SafetySourceDataRepo";
60 
61     private final ArrayMap<SafetySourceKey, SafetySourceData> mSafetySourceData = new ArrayMap<>();
62     private final ArraySet<SafetySourceKey> mSafetySourceErrors = new ArraySet<>();
63     private final ArrayMap<SafetySourceKey, Long> mSafetySourceLastUpdated = new ArrayMap<>();
64     private final ArrayMap<SafetySourceKey, Integer> mSourceStates = new ArrayMap<>();
65 
66     private final SafetyCenterInFlightIssueActionRepository
67             mSafetyCenterInFlightIssueActionRepository;
68     private final SafetyCenterIssueDismissalRepository mSafetyCenterIssueDismissalRepository;
69 
SafetySourceDataRepository( SafetyCenterInFlightIssueActionRepository safetyCenterInFlightIssueActionRepository, SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository)70     SafetySourceDataRepository(
71             SafetyCenterInFlightIssueActionRepository safetyCenterInFlightIssueActionRepository,
72             SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository) {
73         mSafetyCenterInFlightIssueActionRepository = safetyCenterInFlightIssueActionRepository;
74         mSafetyCenterIssueDismissalRepository = safetyCenterIssueDismissalRepository;
75     }
76 
77     /**
78      * Sets the latest {@link SafetySourceData} for the given {@link SafetySourceKey}, and returns
79      * {@code true} if this caused any changes which would alter {@link SafetyCenterData}.
80      *
81      * <p>This method does not perform any validation, {@link
82      * SafetyCenterDataManager#setSafetySourceData(SafetySourceData, String, SafetyEvent, String,
83      * int)} should be called wherever validation is required.
84      *
85      * <p>Setting a {@code null} {@link SafetySourceData} evicts the current {@link
86      * SafetySourceData} entry and clears the {@link SafetyCenterIssueDismissalRepository} for the
87      * source.
88      *
89      * <p>This method may modify the {@link SafetyCenterIssueDismissalRepository}.
90      */
setSafetySourceData( SafetySourceKey safetySourceKey, @Nullable SafetySourceData safetySourceData)91     boolean setSafetySourceData(
92             SafetySourceKey safetySourceKey, @Nullable SafetySourceData safetySourceData) {
93         boolean sourceDataDiffers =
94                 !Objects.equals(safetySourceData, mSafetySourceData.get(safetySourceKey));
95         boolean removedSourceError = mSafetySourceErrors.remove(safetySourceKey);
96 
97         if (sourceDataDiffers) {
98             setSafetySourceDataInternal(safetySourceKey, safetySourceData);
99         }
100 
101         setLastUpdatedNow(safetySourceKey);
102         return sourceDataDiffers || removedSourceError;
103     }
104 
setSafetySourceDataInternal(SafetySourceKey key, @Nullable SafetySourceData data)105     private void setSafetySourceDataInternal(SafetySourceKey key, @Nullable SafetySourceData data) {
106         ArraySet<String> issueIds = new ArraySet<>();
107         if (data == null) {
108             mSafetySourceData.remove(key);
109             mSourceStates.put(key, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_CLEARED);
110         } else {
111             mSafetySourceData.put(key, data);
112             for (int i = 0; i < data.getIssues().size(); i++) {
113                 issueIds.add(data.getIssues().get(i).getId());
114             }
115             mSourceStates.put(key, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__DATA_PROVIDED);
116         }
117         mSafetyCenterIssueDismissalRepository.updateIssuesForSource(
118                 issueIds, key.getSourceId(), key.getUserId());
119     }
120 
121     /**
122      * Returns the latest {@link SafetySourceData} that was set by {@link #setSafetySourceData} for
123      * the given {@link SafetySourceKey}.
124      *
125      * <p>This method does not perform any validation, {@link
126      * SafetyCenterDataManager#getSafetySourceData(String, String, int)} should be called wherever
127      * validation is required.
128      *
129      * <p>Returns {@code null} if it was never set since boot, or if the entry was evicted using
130      * {@link #setSafetySourceData} with a {@code null} value.
131      */
132     @Nullable
getSafetySourceData(SafetySourceKey safetySourceKey)133     SafetySourceData getSafetySourceData(SafetySourceKey safetySourceKey) {
134         return mSafetySourceData.get(safetySourceKey);
135     }
136 
137     /** Returns {@code true} if the given source has an error. */
sourceHasError(SafetySourceKey safetySourceKey)138     boolean sourceHasError(SafetySourceKey safetySourceKey) {
139         return mSafetySourceErrors.contains(safetySourceKey);
140     }
141 
142     /**
143      * Returns whether the repository has the given {@link SafetySourceData} for the given {@link
144      * SafetySourceKey}.
145      */
sourceHasData( SafetySourceKey safetySourceKey, @Nullable SafetySourceData safetySourceData)146     boolean sourceHasData(
147             SafetySourceKey safetySourceKey, @Nullable SafetySourceData safetySourceData) {
148         if (mSafetySourceErrors.contains(safetySourceKey)) {
149             // Any error will cause the SafetySourceData to be discarded in favor of an error
150             // message, so it can't possibly match the SafetySourceData passed in parameter.
151             return false;
152         }
153         return Objects.equals(safetySourceData, mSafetySourceData.get(safetySourceKey));
154     }
155 
156     /**
157      * Reports the given {@link SafetySourceErrorDetails} for the given {@link SafetySourceKey}, and
158      * returns {@code true} if this changed the repository's data.
159      *
160      * <p>This method does not perform any validation, {@link
161      * SafetyCenterDataManager#reportSafetySourceError(SafetySourceErrorDetails, String, String,
162      * int)} should be called wherever validation is required.
163      */
reportSafetySourceError( SafetySourceKey safetySourceKey, SafetySourceErrorDetails safetySourceErrorDetails)164     boolean reportSafetySourceError(
165             SafetySourceKey safetySourceKey, SafetySourceErrorDetails safetySourceErrorDetails) {
166         SafetyEvent safetyEvent = safetySourceErrorDetails.getSafetyEvent();
167         Log.w(
168                 TAG,
169                 "Error reported from source: " + safetySourceKey + ", for event: " + safetyEvent);
170 
171         int safetyEventType = safetyEvent.getType();
172         if (safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED
173                 || safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) {
174             return false;
175         }
176 
177         mSourceStates.put(
178                 safetySourceKey, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__SOURCE_ERROR);
179         return setSafetySourceError(safetySourceKey);
180     }
181 
182     /**
183      * Marks the given {@link SafetySourceKey} as having timed out during a refresh, and returns
184      * {@code true} if it caused a change to the stored data.
185      *
186      * @param setError whether we should clear the data associated with the source and set an error
187      */
markSafetySourceRefreshTimedOut(SafetySourceKey sourceKey, boolean setError)188     boolean markSafetySourceRefreshTimedOut(SafetySourceKey sourceKey, boolean setError) {
189         mSourceStates.put(sourceKey, SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__REFRESH_TIMEOUT);
190         if (!setError) {
191             return false;
192         }
193         return setSafetySourceError(sourceKey);
194     }
195 
196     /**
197      * Marks the given {@link SafetySourceKey} as being in an error state and returns {@code true}
198      * if this changed the repository's data.
199      */
setSafetySourceError(SafetySourceKey safetySourceKey)200     private boolean setSafetySourceError(SafetySourceKey safetySourceKey) {
201         setLastUpdatedNow(safetySourceKey);
202         boolean removingSafetySourceDataChangedSafetyCenterData =
203                 mSafetySourceData.remove(safetySourceKey) != null;
204         boolean addingSafetySourceErrorChangedSafetyCenterData =
205                 mSafetySourceErrors.add(safetySourceKey);
206         return removingSafetySourceDataChangedSafetyCenterData
207                 || addingSafetySourceErrorChangedSafetyCenterData;
208     }
209 
210     /**
211      * Returns the {@link SafetySourceIssue} associated with the given {@link SafetyCenterIssueKey}.
212      *
213      * <p>Returns {@code null} if there is no such {@link SafetySourceIssue}.
214      */
215     @Nullable
getSafetySourceIssue(SafetyCenterIssueKey safetyCenterIssueKey)216     SafetySourceIssue getSafetySourceIssue(SafetyCenterIssueKey safetyCenterIssueKey) {
217         SafetySourceKey key =
218                 SafetySourceKey.of(
219                         safetyCenterIssueKey.getSafetySourceId(), safetyCenterIssueKey.getUserId());
220         SafetySourceData safetySourceData = mSafetySourceData.get(key);
221         if (safetySourceData == null) {
222             return null;
223         }
224         List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues();
225 
226         SafetySourceIssue targetIssue = null;
227         for (int i = 0; i < safetySourceIssues.size(); i++) {
228             SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
229 
230             if (safetyCenterIssueKey.getSafetySourceIssueId().equals(safetySourceIssue.getId())) {
231                 targetIssue = safetySourceIssue;
232                 break;
233             }
234         }
235 
236         return targetIssue;
237     }
238 
239     /**
240      * Returns the {@link SafetySourceIssue.Action} associated with the given {@link
241      * SafetyCenterIssueActionId}.
242      *
243      * <p>Returns {@code null} if there is no associated {@link SafetySourceIssue}.
244      *
245      * <p>Returns {@code null} if the {@link SafetySourceIssue.Action} is currently in flight.
246      */
247     @Nullable
getSafetySourceIssueAction( SafetyCenterIssueActionId safetyCenterIssueActionId)248     SafetySourceIssue.Action getSafetySourceIssueAction(
249             SafetyCenterIssueActionId safetyCenterIssueActionId) {
250         SafetySourceIssue safetySourceIssue =
251                 getSafetySourceIssue(safetyCenterIssueActionId.getSafetyCenterIssueKey());
252 
253         if (safetySourceIssue == null) {
254             return null;
255         }
256 
257         return mSafetyCenterInFlightIssueActionRepository.getSafetySourceIssueAction(
258                 safetyCenterIssueActionId, safetySourceIssue);
259     }
260 
261     /**
262      * Returns the elapsed realtime millis of when the data of the given {@link SafetySourceKey} was
263      * last updated, or {@code 0L} if no update has occurred.
264      *
265      * @see SystemClock#elapsedRealtime()
266      */
267     @UptimeMillisLong
getSafetySourceLastUpdated(SafetySourceKey sourceKey)268     long getSafetySourceLastUpdated(SafetySourceKey sourceKey) {
269         Long lastUpdated = mSafetySourceLastUpdated.get(sourceKey);
270         if (lastUpdated != null) {
271             return lastUpdated;
272         } else {
273             return 0L;
274         }
275     }
276 
setLastUpdatedNow(SafetySourceKey sourceKey)277     private void setLastUpdatedNow(SafetySourceKey sourceKey) {
278         mSafetySourceLastUpdated.put(sourceKey, SystemClock.elapsedRealtime());
279     }
280 
281     /**
282      * Returns the current {@link SafetyCenterStatsdLogger.SourceState} of the given {@link
283      * SafetySourceKey}.
284      */
285     @SafetyCenterStatsdLogger.SourceState
getSourceState(SafetySourceKey sourceKey)286     int getSourceState(SafetySourceKey sourceKey) {
287         Integer sourceState = mSourceStates.get(sourceKey);
288         if (sourceState != null) {
289             return sourceState;
290         } else {
291             return SAFETY_SOURCE_STATE_COLLECTED__SOURCE_STATE__NO_DATA_PROVIDED;
292         }
293     }
294 
295     /** Clears all data for all users. */
clear()296     void clear() {
297         mSafetySourceData.clear();
298         mSafetySourceErrors.clear();
299         mSafetySourceLastUpdated.clear();
300         mSourceStates.clear();
301     }
302 
303     /** Clears all data for the given user. */
clearForUser(@serIdInt int userId)304     void clearForUser(@UserIdInt int userId) {
305         // Loop in reverse index order to be able to remove entries while iterating.
306         for (int i = mSafetySourceData.size() - 1; i >= 0; i--) {
307             SafetySourceKey sourceKey = mSafetySourceData.keyAt(i);
308             if (sourceKey.getUserId() == userId) {
309                 mSafetySourceData.removeAt(i);
310             }
311         }
312         for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) {
313             SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i);
314             if (sourceKey.getUserId() == userId) {
315                 mSafetySourceErrors.removeAt(i);
316             }
317         }
318         for (int i = mSafetySourceLastUpdated.size() - 1; i >= 0; i--) {
319             SafetySourceKey sourceKey = mSafetySourceLastUpdated.keyAt(i);
320             if (sourceKey.getUserId() == userId) {
321                 mSafetySourceLastUpdated.removeAt(i);
322             }
323         }
324         for (int i = mSourceStates.size() - 1; i >= 0; i--) {
325             SafetySourceKey sourceKey = mSourceStates.keyAt(i);
326             if (sourceKey.getUserId() == userId) {
327                 mSourceStates.removeAt(i);
328             }
329         }
330     }
331 
332     /** Dumps state for debugging purposes. */
dump(PrintWriter fout)333     void dump(PrintWriter fout) {
334         dumpArrayMap(fout, mSafetySourceData, "SOURCE DATA");
335         int errorCount = mSafetySourceErrors.size();
336         fout.println("SOURCE ERRORS (" + errorCount + ")");
337         for (int i = 0; i < errorCount; i++) {
338             SafetySourceKey key = mSafetySourceErrors.valueAt(i);
339             fout.println("\t[" + i + "] " + key);
340         }
341         fout.println();
342         dumpArrayMap(fout, mSafetySourceLastUpdated, "LAST UPDATED");
343         dumpArrayMap(fout, mSourceStates, "SOURCE STATES");
344     }
345 
dumpArrayMap(PrintWriter fout, ArrayMap<K, V> map, String label)346     private static <K, V> void dumpArrayMap(PrintWriter fout, ArrayMap<K, V> map, String label) {
347         int count = map.size();
348         fout.println(label + " (" + count + ")");
349         for (int i = 0; i < count; i++) {
350             fout.println("\t[" + i + "] " + map.keyAt(i) + " -> " + map.valueAt(i));
351         }
352         fout.println();
353     }
354 }
355