blob: 50b1a10d3667b35d1e5d7c8d02d61078efda9e2d [file] [log] [blame]
/*
* 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.safetycenter.SafetyCenterManager.RefreshReason;
import android.safetycenter.SafetyCenterStatus;
import android.safetycenter.SafetyCenterStatus.RefreshStatus;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.safetycenter.SafetyCenterConfigReader.Broadcast;
import java.util.List;
import java.util.Objects;
import javax.annotation.concurrent.NotThreadSafe;
/**
* A class to store the state of a refresh of safety sources, if any is ongoing.
*
* <p>This class isn't thread safe. Thread safety must be handled by the caller.
*/
@RequiresApi(TIRAMISU)
@NotThreadSafe
final class SafetyCenterRefreshTracker {
private static final String TAG = "SafetyCenterRefreshTrac";
@NonNull private final SafetyCenterConfigReader mSafetyCenterConfigReader;
@Nullable
// TODO(b/229060064): Should we allow one refresh at a time per UserProfileGroup rather than
// one global refresh?
private RefreshInProgress mRefreshInProgress = null;
/**
* Creates a {@link SafetyCenterRefreshTracker} using the given {@link
* SafetyCenterConfigReader}.
*/
SafetyCenterRefreshTracker(@NonNull SafetyCenterConfigReader safetyCenterConfigReader) {
mSafetyCenterConfigReader = safetyCenterConfigReader;
}
private int mRefreshCounter = 0;
/**
* Reports that a new refresh is in progress and returns the broadcast id associated with this
* refresh.
*/
@NonNull
String reportRefreshInProgress(
@RefreshReason int refreshReason, @NonNull UserProfileGroup userProfileGroup) {
if (mRefreshInProgress != null) {
Log.w(TAG, "Replacing an ongoing refresh");
}
List<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
String refreshBroadcastId =
Objects.hash(refreshReason, broadcasts, userProfileGroup) + "_" + mRefreshCounter++;
Log.v(
TAG,
"Starting a new refresh with refreshReason:"
+ refreshReason
+ " refreshBroadcastId:"
+ refreshBroadcastId);
mRefreshInProgress = new RefreshInProgress(refreshBroadcastId, refreshReason);
for (int i = 0; i < broadcasts.size(); i++) {
Broadcast broadcast = broadcasts.get(i);
List<String> profileOwnerSourceIds =
broadcast.getSourceIdsForProfileOwner(refreshReason);
for (int j = 0; j < profileOwnerSourceIds.size(); j++) {
mRefreshInProgress.addSourceRefreshInFlight(
SafetySourceKey.of(
profileOwnerSourceIds.get(j),
userProfileGroup.getProfileOwnerUserId()));
}
List<String> managedProfilesSourceIds =
broadcast.getSourceIdsForManagedProfiles(refreshReason);
for (int j = 0; j < managedProfilesSourceIds.size(); j++) {
int[] managedProfilesUserIds = userProfileGroup.getManagedProfilesUserIds();
for (int k = 0; k < managedProfilesUserIds.length; k++) {
mRefreshInProgress.addSourceRefreshInFlight(
SafetySourceKey.of(
managedProfilesSourceIds.get(j), managedProfilesUserIds[k]));
}
}
}
return refreshBroadcastId;
}
/** Returns the current refresh status. */
@RefreshStatus
int getRefreshStatus() {
if (mRefreshInProgress == null) {
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;
}
/**
* Reports that a source has completed its refresh, and returns whether this caused the refresh
* to complete.
*
* <p>If a source calls {@code reportSafetySourceError}, then this method is also used to mark
* the refresh as completed.
*/
boolean reportSourceRefreshCompleted(
@NonNull String sourceId, @NonNull String refreshBroadcastId, @UserIdInt int userId) {
if (!checkMethodValid("reportSourceRefreshCompleted", refreshBroadcastId)) {
return false;
}
mRefreshInProgress.markSourceRefreshAsComplete(SafetySourceKey.of(sourceId, userId));
if (!mRefreshInProgress.isComplete()) {
return false;
}
mRefreshInProgress = null;
return true;
}
/**
* Clears any ongoing refresh in progress.
*
* <p>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}.
*/
// TODO(b/229188900): Should we stop any scheduled broadcasts from going out?
void clearRefresh() {
if (mRefreshInProgress != null) {
Log.v(TAG, "Clearing refresh with refreshBroadcastId:" + mRefreshInProgress.getId());
mRefreshInProgress = null;
} else {
Log.v(TAG, "Clear refresh called but no refresh in progress");
}
}
/**
* Clears the refresh in progress with the given id, and returns whether it was ongoing.
*
* <p>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}.
*/
// TODO(b/229188900): Should we stop any scheduled broadcasts from going out?
boolean clearRefresh(@NonNull String refreshBroadcastId) {
if (!checkMethodValid("clearRefresh", refreshBroadcastId)) {
return false;
}
Log.v(TAG, "Clearing refresh with refreshBroadcastId:" + refreshBroadcastId);
mRefreshInProgress = null;
return true;
}
private boolean checkMethodValid(
@NonNull String methodName, @NonNull String refreshBroadcastId) {
if (mRefreshInProgress == null || !mRefreshInProgress.getId().equals(refreshBroadcastId)) {
Log.w(
TAG,
methodName
+ " called for invalid refresh broadcast id: "
+ refreshBroadcastId
+ "; no such refresh in"
+ " progress");
return false;
}
return true;
}
/** Class representing the state of a refresh in progress. */
private static final class RefreshInProgress {
@NonNull private final String mId;
@RefreshReason private final int mReason;
@NonNull private final ArraySet<SafetySourceKey> mSourceRefreshInFlight = new ArraySet<>();
/** Creates a {@link RefreshInProgress}. */
RefreshInProgress(@NonNull String id, @RefreshReason int reason) {
mId = id;
mReason = reason;
}
/**
* Returns the id of the {@link RefreshInProgress}, which corresponds to the {@link
* android.safetycenter.SafetyCenterManager#EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID} used
* in the refresh.
*/
@NonNull
String getId() {
return mId;
}
/** Returns the {@link RefreshReason} that was given for this {@link RefreshInProgress}. */
@RefreshReason
int getReason() {
return mReason;
}
private void addSourceRefreshInFlight(@NonNull SafetySourceKey safetySourceKey) {
mSourceRefreshInFlight.add(safetySourceKey);
Log.v(
TAG,
"Refresh started for sourceId:"
+ safetySourceKey.getSourceId()
+ " userId:"
+ safetySourceKey.getUserId()
+ " with refreshBroadcastId:"
+ mId
+ ", now "
+ mSourceRefreshInFlight.size()
+ " in flight.");
}
private void markSourceRefreshAsComplete(@NonNull SafetySourceKey safetySourceKey) {
mSourceRefreshInFlight.remove(safetySourceKey);
Log.v(
TAG,
"Refresh completed for sourceId:"
+ safetySourceKey.getSourceId()
+ " userId:"
+ safetySourceKey.getUserId()
+ " with refreshBroadcastId:"
+ mId
+ ", "
+ mSourceRefreshInFlight.size()
+ " still in flight.");
}
private boolean isComplete() {
return mSourceRefreshInFlight.isEmpty();
}
}
}