/* * 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.os.Build.VERSION_CODES.TIRAMISU; 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.Nullable; 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.RequiresApi; import com.android.permission.util.UserUtils; 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 */ @RequiresApi(TIRAMISU) @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.w(TAG, "Replacing an ongoing refresh"); } String refreshBroadcastId = UUID.randomUUID() + "_" + mRefreshCounter++; Log.v( TAG, "Starting a new refresh with refreshReason:" + refreshReason + " refreshBroadcastId:" + 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,
            String sourceId,
            @UserIdInt int userId,
            boolean successful,
            boolean dataChanged) {
        RefreshInProgress refreshInProgress =
                getRefreshInProgressWithId("reportSourceRefreshCompleted", refreshBroadcastId);
        if (refreshInProgress == null) {
            return false;
        }
        SafetySourceKey sourceKey = SafetySourceKey.of(sourceId, userId);
        Duration duration =
                refreshInProgress.markSourceRefreshComplete(sourceKey, successful, dataChanged);
        int refreshReason = refreshInProgress.getReason();
        int requestType = RefreshReasons.toRefreshRequestType(refreshReason);
        if (duration != null) {
            int sourceResult = toSystemEventResult(successful);
            SafetyCenterStatsdLogger.writeSourceRefreshSystemEvent(
                    requestType,
                    sourceId,
                    UserUtils.isManagedProfile(userId, 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.v(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.v(TAG, "Clear refresh called but no refresh in progress");
            return null;
        }
        Log.v(TAG, "Clearing refresh with refreshBroadcastId:" + 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 for invalid refresh broadcast id: "
                            + refreshBroadcastId
                            + "; no such refresh in"
                            + " progress");
            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