/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.safetycenter; import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_RESCAN_BUTTON_CLICK; import static com.android.permission.PermissionStatsLog.SAFETY_CENTER_SYSTEM_EVENT_REPORTED__RESULT__TIMEOUT; import static com.android.safetycenter.logging.SafetyCenterStatsdLogger.toSystemEventResult; import android.annotation.ElapsedRealtimeLong; import android.annotation.UserIdInt; import android.content.Context; import android.os.SystemClock; import android.safetycenter.SafetyCenterManager.RefreshReason; import android.safetycenter.SafetyCenterStatus; import android.safetycenter.SafetyCenterStatus.RefreshStatus; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import androidx.annotation.Nullable; import com.android.safetycenter.logging.SafetyCenterStatsdLogger; import java.io.PrintWriter; import java.time.Duration; import java.util.List; import java.util.UUID; import javax.annotation.concurrent.NotThreadSafe; /** * A class to store the state of a refresh of safety sources, if any is ongoing. * *
This class isn't thread safe. Thread safety must be handled by the caller. * * @hide */ @NotThreadSafe public final class SafetyCenterRefreshTracker { private static final String TAG = "SafetyCenterRefreshTrac"; private final Context mContext; @Nullable // TODO(b/229060064): Should we allow one refresh at a time per UserProfileGroup rather than // one global refresh? private RefreshInProgress mRefreshInProgress = null; private int mRefreshCounter = 0; SafetyCenterRefreshTracker(Context context) { mContext = context; } /** * Reports that a new refresh is in progress and returns the broadcast id associated with this * refresh. */ String reportRefreshInProgress( @RefreshReason int refreshReason, UserProfileGroup userProfileGroup) { if (mRefreshInProgress != null) { Log.i(TAG, "Replacing ongoing refresh with id: " + mRefreshInProgress.getId()); } String refreshBroadcastId = UUID.randomUUID() + "_" + mRefreshCounter++; Log.d( TAG, "Starting a new refresh with reason: " + refreshReason + ", and id: " + refreshBroadcastId); mRefreshInProgress = new RefreshInProgress( refreshBroadcastId, refreshReason, userProfileGroup, SafetyCenterFlags.getUntrackedSourceIds()); return refreshBroadcastId; } /** Returns the current refresh status. */ @RefreshStatus int getRefreshStatus() { if (mRefreshInProgress == null || mRefreshInProgress.isComplete()) { return SafetyCenterStatus.REFRESH_STATUS_NONE; } if (mRefreshInProgress.getReason() == REFRESH_REASON_RESCAN_BUTTON_CLICK) { return SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS; } return SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS; } /** * Returns the {@link RefreshReason} for the current refresh, or {@code null} if none is in * progress. */ @RefreshReason @Nullable public Integer getRefreshReason() { if (mRefreshInProgress != null) { return mRefreshInProgress.getReason(); } else { return null; } } /** * Reports that refresh requests have been sent to a collection of sources. * *
When those sources respond call {@link #reportSourceRefreshCompleted} to mark the request
* as complete.
*/
void reportSourceRefreshesInFlight(
String refreshBroadcastId, List If a source calls {@code reportSafetySourceError}, then this method is also used to mark
* the refresh as completed. The {@code successful} parameter indicates whether the refresh
* completed successfully or not. The {@code dataChanged} parameter indicates whether this
* source's data changed or not.
*
* Completed refreshes are logged to statsd.
*/
public boolean reportSourceRefreshCompleted(
String refreshBroadcastId,
SafetySourceKey safetySourceKey,
boolean successful,
boolean dataChanged) {
RefreshInProgress refreshInProgress =
getRefreshInProgressWithId("reportSourceRefreshCompleted", refreshBroadcastId);
if (refreshInProgress == null) {
return false;
}
Duration duration =
refreshInProgress.markSourceRefreshComplete(
safetySourceKey, successful, dataChanged);
int refreshReason = refreshInProgress.getReason();
int requestType = RefreshReasons.toRefreshRequestType(refreshReason);
if (duration != null) {
int sourceResult = toSystemEventResult(successful);
SafetyCenterStatsdLogger.writeSourceRefreshSystemEvent(
requestType,
safetySourceKey.getSourceId(),
UserProfileGroup.getProfileTypeOfUser(safetySourceKey.getUserId(), mContext),
duration,
sourceResult,
refreshReason,
dataChanged);
}
if (!refreshInProgress.isComplete()) {
return false;
}
Log.v(TAG, "Refresh with id: " + refreshInProgress.getId() + " completed");
int wholeResult =
toSystemEventResult(/* success= */ !refreshInProgress.hasAnyTrackedSourceErrors());
SafetyCenterStatsdLogger.writeWholeRefreshSystemEvent(
requestType,
refreshInProgress.getDurationSinceStart(),
wholeResult,
refreshReason,
refreshInProgress.hasAnyTrackedSourceDataChanged());
mRefreshInProgress = null;
return true;
}
/**
* Clears any ongoing refresh in progress, if any.
*
* Note that this method simply clears the tracking of a refresh, and does not prevent
* scheduled broadcasts being sent by {@link
* android.safetycenter.SafetyCenterManager#refreshSafetySources}.
*/
void clearRefresh() {
clearRefreshInternal();
}
/**
* Clears the refresh in progress, if there is any with the given id.
*
* Note that this method simply clears the tracking of a refresh, and does not prevent
* scheduled broadcasts being sent by {@link
* android.safetycenter.SafetyCenterManager#refreshSafetySources}.
*/
void clearRefresh(String refreshBroadcastId) {
if (!checkRefreshInProgress("clearRefresh", refreshBroadcastId)) {
return;
}
clearRefreshInternal();
}
/**
* Clears any ongoing refresh in progress for the given user.
*
* Note that this method simply clears the tracking of a refresh, and does not prevent
* scheduled broadcasts being sent by {@link
* android.safetycenter.SafetyCenterManager#refreshSafetySources}.
*/
void clearRefreshForUser(@UserIdInt int userId) {
if (mRefreshInProgress == null) {
Log.d(TAG, "Clear refresh for user called but no refresh in progress");
return;
}
if (mRefreshInProgress.clearForUser(userId)) {
clearRefreshInternal();
}
}
/**
* Clears the refresh in progress with the given id, and returns the {@link SafetySourceKey}s
* that were still in-flight prior to doing that, if any.
*
* Returns {@code null} if there was no refresh in progress with the given {@code
* refreshBroadcastId}, or if it was already complete.
*
* Note that this method simply clears the tracking of a refresh, and does not prevent
* scheduled broadcasts being sent by {@link
* android.safetycenter.SafetyCenterManager#refreshSafetySources}.
*/
@Nullable
ArraySet If there was no refresh in progress then {@code null} is returned.
*/
@Nullable
private RefreshInProgress clearRefreshInternal() {
RefreshInProgress refreshToClear = mRefreshInProgress;
if (refreshToClear == null) {
Log.d(TAG, "Clear refresh called but no refresh in progress");
return null;
}
Log.v(TAG, "Clearing refresh with id: " + refreshToClear.getId());
mRefreshInProgress = null;
return refreshToClear;
}
/**
* Returns the current {@link RefreshInProgress} if it has the given ID, or logs and returns
* {@code null} if not.
*/
@Nullable
private RefreshInProgress getRefreshInProgressWithId(
String methodName, String refreshBroadcastId) {
RefreshInProgress refreshInProgress = mRefreshInProgress;
if (refreshInProgress == null || !refreshInProgress.getId().equals(refreshBroadcastId)) {
Log.i(TAG, methodName + " called with invalid refresh id: " + refreshBroadcastId);
return null;
}
return refreshInProgress;
}
private boolean checkRefreshInProgress(String methodName, String refreshBroadcastId) {
return getRefreshInProgressWithId(methodName, refreshBroadcastId) != null;
}
/** Dumps state for debugging purposes. */
void dump(PrintWriter fout) {
fout.println(
"REFRESH IN PROGRESS ("
+ (mRefreshInProgress != null)
+ ", counter="
+ mRefreshCounter
+ ")");
if (mRefreshInProgress != null) {
fout.println("\t" + mRefreshInProgress);
}
fout.println();
}
/** Class representing the state of a refresh in progress. */
private static final class RefreshInProgress {
private final String mId;
@RefreshReason private final int mReason;
private final UserProfileGroup mUserProfileGroup;
private final ArraySet