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