• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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;
18 
19 import static android.os.Build.VERSION_CODES.TIRAMISU;
20 import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_RESCAN_BUTTON_CLICK;
21 
22 import static com.android.permission.PermissionStatsLog.SAFETY_CENTER_SYSTEM_EVENT_REPORTED__RESULT__TIMEOUT;
23 import static com.android.safetycenter.logging.SafetyCenterStatsdLogger.toSystemEventResult;
24 
25 import android.annotation.ElapsedRealtimeLong;
26 import android.annotation.Nullable;
27 import android.annotation.UserIdInt;
28 import android.content.Context;
29 import android.os.SystemClock;
30 import android.safetycenter.SafetyCenterManager.RefreshReason;
31 import android.safetycenter.SafetyCenterStatus;
32 import android.safetycenter.SafetyCenterStatus.RefreshStatus;
33 import android.util.ArrayMap;
34 import android.util.ArraySet;
35 import android.util.Log;
36 
37 import androidx.annotation.RequiresApi;
38 
39 import com.android.permission.util.UserUtils;
40 import com.android.safetycenter.logging.SafetyCenterStatsdLogger;
41 
42 import java.io.PrintWriter;
43 import java.time.Duration;
44 import java.util.List;
45 import java.util.UUID;
46 
47 import javax.annotation.concurrent.NotThreadSafe;
48 
49 /**
50  * A class to store the state of a refresh of safety sources, if any is ongoing.
51  *
52  * <p>This class isn't thread safe. Thread safety must be handled by the caller.
53  *
54  * @hide
55  */
56 @RequiresApi(TIRAMISU)
57 @NotThreadSafe
58 public final class SafetyCenterRefreshTracker {
59     private static final String TAG = "SafetyCenterRefreshTrac";
60 
61     private final Context mContext;
62 
63     @Nullable
64     // TODO(b/229060064): Should we allow one refresh at a time per UserProfileGroup rather than
65     //  one global refresh?
66     private RefreshInProgress mRefreshInProgress = null;
67 
68     private int mRefreshCounter = 0;
69 
SafetyCenterRefreshTracker(Context context)70     SafetyCenterRefreshTracker(Context context) {
71         mContext = context;
72     }
73 
74     /**
75      * Reports that a new refresh is in progress and returns the broadcast id associated with this
76      * refresh.
77      */
reportRefreshInProgress( @efreshReason int refreshReason, UserProfileGroup userProfileGroup)78     String reportRefreshInProgress(
79             @RefreshReason int refreshReason, UserProfileGroup userProfileGroup) {
80         if (mRefreshInProgress != null) {
81             Log.w(TAG, "Replacing an ongoing refresh");
82         }
83 
84         String refreshBroadcastId = UUID.randomUUID() + "_" + mRefreshCounter++;
85         Log.v(
86                 TAG,
87                 "Starting a new refresh with refreshReason:"
88                         + refreshReason
89                         + " refreshBroadcastId:"
90                         + refreshBroadcastId);
91 
92         mRefreshInProgress =
93                 new RefreshInProgress(
94                         refreshBroadcastId,
95                         refreshReason,
96                         userProfileGroup,
97                         SafetyCenterFlags.getUntrackedSourceIds());
98 
99         return refreshBroadcastId;
100     }
101 
102     /** Returns the current refresh status. */
103     @RefreshStatus
getRefreshStatus()104     int getRefreshStatus() {
105         if (mRefreshInProgress == null || mRefreshInProgress.isComplete()) {
106             return SafetyCenterStatus.REFRESH_STATUS_NONE;
107         }
108 
109         if (mRefreshInProgress.getReason() == REFRESH_REASON_RESCAN_BUTTON_CLICK) {
110             return SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS;
111         }
112         return SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS;
113     }
114 
115     /**
116      * Returns the {@link RefreshReason} for the current refresh, or {@code null} if none is in
117      * progress.
118      */
119     @RefreshReason
120     @Nullable
getRefreshReason()121     public Integer getRefreshReason() {
122         if (mRefreshInProgress != null) {
123             return mRefreshInProgress.getReason();
124         } else {
125             return null;
126         }
127     }
128 
129     /**
130      * Reports that refresh requests have been sent to a collection of sources.
131      *
132      * <p>When those sources respond call {@link #reportSourceRefreshCompleted} to mark the request
133      * as complete.
134      */
reportSourceRefreshesInFlight( String refreshBroadcastId, List<String> sourceIds, @UserIdInt int userId)135     void reportSourceRefreshesInFlight(
136             String refreshBroadcastId, List<String> sourceIds, @UserIdInt int userId) {
137         RefreshInProgress refreshInProgress =
138                 getRefreshInProgressWithId("reportSourceRefreshesInFlight", refreshBroadcastId);
139         if (refreshInProgress == null) {
140             return;
141         }
142         for (int i = 0; i < sourceIds.size(); i++) {
143             SafetySourceKey key = SafetySourceKey.of(sourceIds.get(i), userId);
144             refreshInProgress.markSourceRefreshInFlight(key);
145         }
146     }
147 
148     /**
149      * Reports that a source has completed its refresh, and returns {@code true} if the whole
150      * current refresh is now complete.
151      *
152      * <p>If a source calls {@code reportSafetySourceError}, then this method is also used to mark
153      * the refresh as completed. The {@code successful} parameter indicates whether the refresh
154      * completed successfully or not. The {@code dataChanged} parameter indicates whether this
155      * source's data changed or not.
156      *
157      * <p>Completed refreshes are logged to statsd.
158      */
reportSourceRefreshCompleted( String refreshBroadcastId, String sourceId, @UserIdInt int userId, boolean successful, boolean dataChanged)159     public boolean reportSourceRefreshCompleted(
160             String refreshBroadcastId,
161             String sourceId,
162             @UserIdInt int userId,
163             boolean successful,
164             boolean dataChanged) {
165         RefreshInProgress refreshInProgress =
166                 getRefreshInProgressWithId("reportSourceRefreshCompleted", refreshBroadcastId);
167         if (refreshInProgress == null) {
168             return false;
169         }
170 
171         SafetySourceKey sourceKey = SafetySourceKey.of(sourceId, userId);
172         Duration duration =
173                 refreshInProgress.markSourceRefreshComplete(sourceKey, successful, dataChanged);
174         int refreshReason = refreshInProgress.getReason();
175         int requestType = RefreshReasons.toRefreshRequestType(refreshReason);
176 
177         if (duration != null) {
178             int sourceResult = toSystemEventResult(successful);
179             SafetyCenterStatsdLogger.writeSourceRefreshSystemEvent(
180                     requestType,
181                     sourceId,
182                     UserUtils.isManagedProfile(userId, mContext),
183                     duration,
184                     sourceResult,
185                     refreshReason,
186                     dataChanged);
187         }
188 
189         if (!refreshInProgress.isComplete()) {
190             return false;
191         }
192 
193         Log.v(TAG, "Refresh with id: " + refreshInProgress.getId() + " completed");
194         int wholeResult =
195                 toSystemEventResult(/* success= */ !refreshInProgress.hasAnyTrackedSourceErrors());
196         SafetyCenterStatsdLogger.writeWholeRefreshSystemEvent(
197                 requestType,
198                 refreshInProgress.getDurationSinceStart(),
199                 wholeResult,
200                 refreshReason,
201                 refreshInProgress.hasAnyTrackedSourceDataChanged());
202         mRefreshInProgress = null;
203         return true;
204     }
205 
206     /**
207      * Clears any ongoing refresh in progress, if any.
208      *
209      * <p>Note that this method simply clears the tracking of a refresh, and does not prevent
210      * scheduled broadcasts being sent by {@link
211      * android.safetycenter.SafetyCenterManager#refreshSafetySources}.
212      */
clearRefresh()213     void clearRefresh() {
214         clearRefreshInternal();
215     }
216 
217     /**
218      * Clears the refresh in progress, if there is any with the given id.
219      *
220      * <p>Note that this method simply clears the tracking of a refresh, and does not prevent
221      * scheduled broadcasts being sent by {@link
222      * android.safetycenter.SafetyCenterManager#refreshSafetySources}.
223      */
clearRefresh(String refreshBroadcastId)224     void clearRefresh(String refreshBroadcastId) {
225         if (!checkRefreshInProgress("clearRefresh", refreshBroadcastId)) {
226             return;
227         }
228         clearRefreshInternal();
229     }
230 
231     /**
232      * Clears any ongoing refresh in progress for the given user.
233      *
234      * <p>Note that this method simply clears the tracking of a refresh, and does not prevent
235      * scheduled broadcasts being sent by {@link
236      * android.safetycenter.SafetyCenterManager#refreshSafetySources}.
237      */
clearRefreshForUser(@serIdInt int userId)238     void clearRefreshForUser(@UserIdInt int userId) {
239         if (mRefreshInProgress == null) {
240             Log.v(TAG, "Clear refresh for user called but no refresh in progress");
241             return;
242         }
243         if (mRefreshInProgress.clearForUser(userId)) {
244             clearRefreshInternal();
245         }
246     }
247 
248     /**
249      * Clears the refresh in progress with the given id, and returns the {@link SafetySourceKey}s
250      * that were still in-flight prior to doing that, if any.
251      *
252      * <p>Returns {@code null} if there was no refresh in progress with the given {@code
253      * refreshBroadcastId}, or if it was already complete.
254      *
255      * <p>Note that this method simply clears the tracking of a refresh, and does not prevent
256      * scheduled broadcasts being sent by {@link
257      * android.safetycenter.SafetyCenterManager#refreshSafetySources}.
258      */
259     @Nullable
timeoutRefresh(String refreshBroadcastId)260     ArraySet<SafetySourceKey> timeoutRefresh(String refreshBroadcastId) {
261         if (!checkRefreshInProgress("timeoutRefresh", refreshBroadcastId)) {
262             return null;
263         }
264 
265         RefreshInProgress clearedRefresh = clearRefreshInternal();
266 
267         if (clearedRefresh == null || clearedRefresh.isComplete()) {
268             return null;
269         }
270 
271         ArraySet<SafetySourceKey> timedOutSources = clearedRefresh.getSourceRefreshesInFlight();
272         int refreshReason = clearedRefresh.getReason();
273         int requestType = RefreshReasons.toRefreshRequestType(refreshReason);
274 
275         for (int i = 0; i < timedOutSources.size(); i++) {
276             SafetySourceKey sourceKey = timedOutSources.valueAt(i);
277             Duration duration = clearedRefresh.getDurationSinceSourceStart(sourceKey);
278             if (duration != null) {
279                 SafetyCenterStatsdLogger.writeSourceRefreshSystemEvent(
280                         requestType,
281                         sourceKey.getSourceId(),
282                         UserUtils.isManagedProfile(sourceKey.getUserId(), mContext),
283                         duration,
284                         SAFETY_CENTER_SYSTEM_EVENT_REPORTED__RESULT__TIMEOUT,
285                         refreshReason,
286                         false);
287             }
288         }
289 
290         SafetyCenterStatsdLogger.writeWholeRefreshSystemEvent(
291                 requestType,
292                 clearedRefresh.getDurationSinceStart(),
293                 SAFETY_CENTER_SYSTEM_EVENT_REPORTED__RESULT__TIMEOUT,
294                 refreshReason,
295                 clearedRefresh.hasAnyTrackedSourceDataChanged());
296 
297         return timedOutSources;
298     }
299 
300     /**
301      * Clears the any refresh in progress and returns it for the caller to do what it needs to.
302      *
303      * <p>If there was no refresh in progress then {@code null} is returned.
304      */
305     @Nullable
clearRefreshInternal()306     private RefreshInProgress clearRefreshInternal() {
307         RefreshInProgress refreshToClear = mRefreshInProgress;
308         if (refreshToClear == null) {
309             Log.v(TAG, "Clear refresh called but no refresh in progress");
310             return null;
311         }
312 
313         Log.v(TAG, "Clearing refresh with refreshBroadcastId:" + refreshToClear.getId());
314         mRefreshInProgress = null;
315         return refreshToClear;
316     }
317 
318     /**
319      * Returns the current {@link RefreshInProgress} if it has the given ID, or logs and returns
320      * {@code null} if not.
321      */
322     @Nullable
getRefreshInProgressWithId( String methodName, String refreshBroadcastId)323     private RefreshInProgress getRefreshInProgressWithId(
324             String methodName, String refreshBroadcastId) {
325         RefreshInProgress refreshInProgress = mRefreshInProgress;
326         if (refreshInProgress == null || !refreshInProgress.getId().equals(refreshBroadcastId)) {
327             Log.i(
328                     TAG,
329                     methodName
330                             + " called for invalid refresh broadcast id: "
331                             + refreshBroadcastId
332                             + "; no such refresh in"
333                             + " progress");
334             return null;
335         }
336         return refreshInProgress;
337     }
338 
checkRefreshInProgress(String methodName, String refreshBroadcastId)339     private boolean checkRefreshInProgress(String methodName, String refreshBroadcastId) {
340         return getRefreshInProgressWithId(methodName, refreshBroadcastId) != null;
341     }
342 
343     /** Dumps state for debugging purposes. */
dump(PrintWriter fout)344     void dump(PrintWriter fout) {
345         fout.println(
346                 "REFRESH IN PROGRESS ("
347                         + (mRefreshInProgress != null)
348                         + ", counter="
349                         + mRefreshCounter
350                         + ")");
351         if (mRefreshInProgress != null) {
352             fout.println("\t" + mRefreshInProgress);
353         }
354         fout.println();
355     }
356 
357     /** Class representing the state of a refresh in progress. */
358     private static final class RefreshInProgress {
359 
360         private final String mId;
361         @RefreshReason private final int mReason;
362         private final UserProfileGroup mUserProfileGroup;
363         private final ArraySet<String> mUntrackedSourcesIds;
364         @ElapsedRealtimeLong private final long mStartElapsedMillis;
365 
366         // The values in this map are the start times of each source refresh. The alternative of
367         // using mStartTime as the start time of all source refreshes was considered, but this
368         // approach is less sensitive to delays/implementation changes in broadcast dispatch.
369         private final ArrayMap<SafetySourceKey, Long> mSourceRefreshesInFlight = new ArrayMap<>();
370 
371         private boolean mAnyTrackedSourceErrors = false;
372         private boolean mAnyTrackedSourceDataChanged = false;
373 
RefreshInProgress( String id, @RefreshReason int reason, UserProfileGroup userProfileGroup, ArraySet<String> untrackedSourceIds)374         RefreshInProgress(
375                 String id,
376                 @RefreshReason int reason,
377                 UserProfileGroup userProfileGroup,
378                 ArraySet<String> untrackedSourceIds) {
379             mId = id;
380             mReason = reason;
381             mUserProfileGroup = userProfileGroup;
382             mUntrackedSourcesIds = untrackedSourceIds;
383             mStartElapsedMillis = SystemClock.elapsedRealtime();
384         }
385 
386         /**
387          * Returns the id of the {@link RefreshInProgress}, which corresponds to the {@link
388          * android.safetycenter.SafetyCenterManager#EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID} used
389          * in the refresh.
390          */
getId()391         private String getId() {
392             return mId;
393         }
394 
395         /** Returns the {@link RefreshReason} that was given for this {@link RefreshInProgress}. */
396         @RefreshReason
getReason()397         private int getReason() {
398             return mReason;
399         }
400 
401         /** Returns the {@link Duration} since this refresh started. */
getDurationSinceStart()402         private Duration getDurationSinceStart() {
403             return Duration.ofMillis(SystemClock.elapsedRealtime() - mStartElapsedMillis);
404         }
405 
406         @Nullable
getDurationSinceSourceStart(SafetySourceKey safetySourceKey)407         private Duration getDurationSinceSourceStart(SafetySourceKey safetySourceKey) {
408             Long startElapsedMillis = mSourceRefreshesInFlight.get(safetySourceKey);
409             if (startElapsedMillis == null) {
410                 return null;
411             }
412             return Duration.ofMillis(SystemClock.elapsedRealtime() - startElapsedMillis);
413         }
414 
415         /** Returns the {@link SafetySourceKey} of all in-flight source refreshes. */
getSourceRefreshesInFlight()416         private ArraySet<SafetySourceKey> getSourceRefreshesInFlight() {
417             return new ArraySet<>(mSourceRefreshesInFlight.keySet());
418         }
419 
420         /** Returns {@code true} if any refresh of a tracked source completed with an error. */
hasAnyTrackedSourceErrors()421         private boolean hasAnyTrackedSourceErrors() {
422             return mAnyTrackedSourceErrors;
423         }
424 
425         /** Returns {@code true} if any refresh of a tracked source changed that source's data. */
hasAnyTrackedSourceDataChanged()426         private boolean hasAnyTrackedSourceDataChanged() {
427             return mAnyTrackedSourceDataChanged;
428         }
429 
markSourceRefreshInFlight(SafetySourceKey safetySourceKey)430         private void markSourceRefreshInFlight(SafetySourceKey safetySourceKey) {
431             boolean tracked = isTracked(safetySourceKey);
432             long currentElapsedMillis = SystemClock.elapsedRealtime();
433             if (tracked) {
434                 mSourceRefreshesInFlight.put(safetySourceKey, currentElapsedMillis);
435             }
436             Log.v(
437                     TAG,
438                     "Refresh started for sourceId:"
439                             + safetySourceKey.getSourceId()
440                             + " userId:"
441                             + safetySourceKey.getUserId()
442                             + " with refreshBroadcastId:"
443                             + mId
444                             + " at currentElapsedMillis:"
445                             + currentElapsedMillis
446                             + " & tracking:"
447                             + tracked
448                             + ", now "
449                             + mSourceRefreshesInFlight.size()
450                             + " tracked sources in flight.");
451         }
452 
453         @Nullable
markSourceRefreshComplete( SafetySourceKey safetySourceKey, boolean successful, boolean dataChanged)454         private Duration markSourceRefreshComplete(
455                 SafetySourceKey safetySourceKey, boolean successful, boolean dataChanged) {
456             Long startElapsedMillis = mSourceRefreshesInFlight.remove(safetySourceKey);
457 
458             boolean tracked = isTracked(safetySourceKey);
459             mAnyTrackedSourceErrors |= (tracked && !successful);
460             mAnyTrackedSourceDataChanged |= dataChanged;
461             Duration duration =
462                     (startElapsedMillis == null)
463                             ? null
464                             : Duration.ofMillis(SystemClock.elapsedRealtime() - startElapsedMillis);
465             Log.v(
466                     TAG,
467                     "Refresh completed for sourceId:"
468                             + safetySourceKey.getSourceId()
469                             + " userId:"
470                             + safetySourceKey.getUserId()
471                             + " with refreshBroadcastId:"
472                             + mId
473                             + " duration:"
474                             + duration
475                             + " successful:"
476                             + successful
477                             + " dataChanged:"
478                             + dataChanged
479                             + " & tracking:"
480                             + tracked
481                             + ", "
482                             + mSourceRefreshesInFlight.size()
483                             + " tracked sources still in flight.");
484             return duration;
485         }
486 
isTracked(SafetySourceKey safetySourceKey)487         private boolean isTracked(SafetySourceKey safetySourceKey) {
488             return !mUntrackedSourcesIds.contains(safetySourceKey.getSourceId());
489         }
490 
491         /**
492          * Clears the data for the given {@code userId} and returns whether that caused the entire
493          * refresh to complete.
494          */
clearForUser(@serIdInt int userId)495         private boolean clearForUser(@UserIdInt int userId) {
496             if (mUserProfileGroup.getProfileParentUserId() == userId) {
497                 return true;
498             }
499             // Loop in reverse index order to be able to remove entries while iterating.
500             for (int i = mSourceRefreshesInFlight.size() - 1; i >= 0; i--) {
501                 SafetySourceKey sourceKey = mSourceRefreshesInFlight.keyAt(i);
502                 if (sourceKey.getUserId() == userId) {
503                     mSourceRefreshesInFlight.removeAt(i);
504                 }
505             }
506             return isComplete();
507         }
508 
isComplete()509         private boolean isComplete() {
510             return mSourceRefreshesInFlight.isEmpty();
511         }
512 
513         @Override
toString()514         public String toString() {
515             return "RefreshInProgress{"
516                     + "mId='"
517                     + mId
518                     + '\''
519                     + ", mReason="
520                     + mReason
521                     + ", mUserProfileGroup="
522                     + mUserProfileGroup
523                     + ", mUntrackedSourcesIds="
524                     + mUntrackedSourcesIds
525                     + ", mSourceRefreshesInFlight="
526                     + mSourceRefreshesInFlight
527                     + ", mStartElapsedMillis="
528                     + mStartElapsedMillis
529                     + ", mAnyTrackedSourceErrors="
530                     + mAnyTrackedSourceErrors
531                     + ", mAnyTrackedSourceDataChanged="
532                     + mAnyTrackedSourceDataChanged
533                     + '}';
534         }
535     }
536 }
537