| /* |
| * 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 com.android.safetycenter.WestworldLogger.toSystemEventResult; |
| import static com.android.safetycenter.internaldata.SafetyCenterIds.toUserFriendlyString; |
| |
| import static java.util.Collections.emptyList; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.UserIdInt; |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.icu.text.ListFormatter; |
| import android.icu.text.MessageFormat; |
| import android.icu.util.ULocale; |
| import android.os.SystemClock; |
| import android.safetycenter.SafetyCenterData; |
| import android.safetycenter.SafetyCenterEntry; |
| import android.safetycenter.SafetyCenterEntryGroup; |
| import android.safetycenter.SafetyCenterEntryOrGroup; |
| import android.safetycenter.SafetyCenterIssue; |
| import android.safetycenter.SafetyCenterStaticEntry; |
| import android.safetycenter.SafetyCenterStaticEntryGroup; |
| import android.safetycenter.SafetyCenterStatus; |
| import android.safetycenter.SafetyEvent; |
| import android.safetycenter.SafetySourceData; |
| import android.safetycenter.SafetySourceErrorDetails; |
| import android.safetycenter.SafetySourceIssue; |
| import android.safetycenter.SafetySourceStatus; |
| import android.safetycenter.config.SafetyCenterConfig; |
| import android.safetycenter.config.SafetySource; |
| import android.safetycenter.config.SafetySourcesGroup; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.StatsEvent; |
| |
| import androidx.annotation.RequiresApi; |
| |
| import com.android.permission.PermissionStatsLog; |
| import com.android.permission.util.UserUtils; |
| import com.android.safetycenter.SafetyCenterConfigReader.ExternalSafetySource; |
| import com.android.safetycenter.WestworldLogger.SystemEventResult; |
| import com.android.safetycenter.internaldata.SafetyCenterEntryGroupId; |
| import com.android.safetycenter.internaldata.SafetyCenterEntryId; |
| import com.android.safetycenter.internaldata.SafetyCenterIds; |
| import com.android.safetycenter.internaldata.SafetyCenterIssueActionId; |
| import com.android.safetycenter.internaldata.SafetyCenterIssueId; |
| import com.android.safetycenter.internaldata.SafetyCenterIssueKey; |
| import com.android.safetycenter.resources.SafetyCenterResourcesContext; |
| |
| import java.io.PrintWriter; |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Objects; |
| |
| import javax.annotation.concurrent.NotThreadSafe; |
| |
| /** |
| * A class that keeps track of all the {@link SafetySourceData} set by safety sources, and |
| * aggregates them into a {@link SafetyCenterData} object to be used by PermissionController. |
| * |
| * <p>This class isn't thread safe. Thread safety must be handled by the caller. |
| */ |
| @RequiresApi(TIRAMISU) |
| @NotThreadSafe |
| final class SafetyCenterDataTracker { |
| |
| private static final String TAG = "SafetyCenterDataTracker"; |
| |
| private static final String ANDROID_LOCK_SCREEN_SOURCES_GROUP_ID = "AndroidLockScreenSources"; |
| |
| private static final SafetyCenterIssuesBySeverityDescending |
| SAFETY_CENTER_ISSUES_BY_SEVERITY_DESCENDING = |
| new SafetyCenterIssuesBySeverityDescending(); |
| |
| private final ArrayMap<SafetySourceKey, SafetySourceData> mSafetySourceDataForKey = |
| new ArrayMap<>(); |
| |
| private final ArraySet<SafetySourceKey> mSafetySourceErrors = new ArraySet<>(); |
| |
| private final ArrayMap<SafetyCenterIssueActionId, Long> mSafetyCenterIssueActionsInFlight = |
| new ArrayMap<>(); |
| |
| @NonNull private final Context mContext; |
| @NonNull private final SafetyCenterResourcesContext mSafetyCenterResourcesContext; |
| @NonNull private final SafetyCenterConfigReader mSafetyCenterConfigReader; |
| @NonNull private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker; |
| @NonNull private final WestworldLogger mWestworldLogger; |
| @NonNull private final PendingIntentFactory mPendingIntentFactory; |
| @NonNull private final SafetyCenterIssueCache mSafetyCenterIssueCache; |
| |
| SafetyCenterDataTracker( |
| @NonNull Context context, |
| @NonNull SafetyCenterResourcesContext safetyCenterResourcesContext, |
| @NonNull SafetyCenterConfigReader safetyCenterConfigReader, |
| @NonNull SafetyCenterRefreshTracker safetyCenterRefreshTracker, |
| @NonNull WestworldLogger westworldLogger, |
| @NonNull PendingIntentFactory pendingIntentFactory, |
| @NonNull SafetyCenterIssueCache safetyCenterIssueCache) { |
| mContext = context; |
| mSafetyCenterResourcesContext = safetyCenterResourcesContext; |
| mSafetyCenterConfigReader = safetyCenterConfigReader; |
| mSafetyCenterRefreshTracker = safetyCenterRefreshTracker; |
| mWestworldLogger = westworldLogger; |
| mPendingIntentFactory = pendingIntentFactory; |
| mSafetyCenterIssueCache = safetyCenterIssueCache; |
| } |
| |
| /** |
| * Sets the latest {@link SafetySourceData} for the given {@code safetySourceId}, {@link |
| * SafetyEvent}, {@code packageName} and {@code userId}, and returns whether there was a change |
| * to the underlying {@link SafetyCenterData}. |
| * |
| * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code |
| * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected; or the {@link |
| * SafetySourceData} does not respect all constraints defined in the config. |
| * |
| * <p>Setting a {@code null} {@link SafetySourceData} evicts the current {@link |
| * SafetySourceData} entry and clears the Safety Center issue cache for the source. |
| * |
| * <p>This method may modify the {@link SafetyCenterIssueCache}. |
| */ |
| boolean setSafetySourceData( |
| @Nullable SafetySourceData safetySourceData, |
| @NonNull String safetySourceId, |
| @NonNull SafetyEvent safetyEvent, |
| @NonNull String packageName, |
| @UserIdInt int userId) { |
| if (!validateRequest(safetySourceData, safetySourceId, packageName, userId)) { |
| return false; |
| } |
| boolean safetyEventChangedSafetyCenterData = |
| processSafetyEvent(safetySourceId, safetyEvent, userId, false); |
| |
| SafetySourceKey key = SafetySourceKey.of(safetySourceId, userId); |
| boolean removingSafetySourceErrorChangedSafetyCenterData = mSafetySourceErrors.remove(key); |
| SafetySourceData existingSafetySourceData = mSafetySourceDataForKey.get(key); |
| if (Objects.equals(safetySourceData, existingSafetySourceData)) { |
| return safetyEventChangedSafetyCenterData |
| || removingSafetySourceErrorChangedSafetyCenterData; |
| } |
| |
| ArraySet<String> issueIds = new ArraySet<>(); |
| if (safetySourceData == null) { |
| mSafetySourceDataForKey.remove(key); |
| } else { |
| mSafetySourceDataForKey.put(key, safetySourceData); |
| for (int i = 0; i < safetySourceData.getIssues().size(); i++) { |
| issueIds.add(safetySourceData.getIssues().get(i).getId()); |
| } |
| } |
| mSafetyCenterIssueCache.updateIssuesForSource(issueIds, safetySourceId, userId); |
| |
| return true; |
| } |
| |
| /** |
| * Returns the latest {@link SafetySourceData} that was set by {@link #setSafetySourceData} for |
| * the given {@code safetySourceId}, {@code packageName} and {@code userId}. |
| * |
| * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code |
| * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected. |
| * |
| * <p>Returns {@code null} if it was never set since boot, or if the entry was evicted using |
| * {@link #setSafetySourceData} with a {@code null} value. |
| */ |
| @Nullable |
| SafetySourceData getSafetySourceData( |
| @NonNull String safetySourceId, @NonNull String packageName, @UserIdInt int userId) { |
| if (!validateRequest(null, safetySourceId, packageName, userId)) { |
| return null; |
| } |
| return mSafetySourceDataForKey.get(SafetySourceKey.of(safetySourceId, userId)); |
| } |
| |
| /** |
| * Reports the given {@link SafetySourceErrorDetails} for the given {@code safetySourceId} and |
| * {@code userId}, and returns whether there was a change to the underlying {@link |
| * SafetyCenterData}. |
| * |
| * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code |
| * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected. |
| */ |
| boolean reportSafetySourceError( |
| @NonNull SafetySourceErrorDetails safetySourceErrorDetails, |
| @NonNull String safetySourceId, |
| @NonNull String packageName, |
| @UserIdInt int userId) { |
| if (!validateRequest(null, safetySourceId, packageName, userId)) { |
| return false; |
| } |
| SafetyEvent safetyEvent = safetySourceErrorDetails.getSafetyEvent(); |
| Log.w(TAG, "Error reported from source: " + safetySourceId + ", for event: " + safetyEvent); |
| |
| boolean safetyEventChangedSafetyCenterData = |
| processSafetyEvent(safetySourceId, safetyEvent, userId, true); |
| int safetyEventType = safetyEvent.getType(); |
| if (safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED |
| || safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) { |
| return safetyEventChangedSafetyCenterData; |
| } |
| |
| SafetySourceKey key = SafetySourceKey.of(safetySourceId, userId); |
| boolean safetySourceErrorChangedSafetyCenterData = setSafetySourceError(key); |
| return safetyEventChangedSafetyCenterData || safetySourceErrorChangedSafetyCenterData; |
| } |
| |
| /** Marks the given {@link SafetySourceKey} as having errored-out. */ |
| boolean setSafetySourceError(@NonNull SafetySourceKey safetySourceKey) { |
| boolean removingSafetySourceDataChangedSafetyCenterData = |
| mSafetySourceDataForKey.remove(safetySourceKey) != null; |
| boolean addingSafetySourceErrorChangedSafetyCenterData = |
| mSafetySourceErrors.add(safetySourceKey); |
| return removingSafetySourceDataChangedSafetyCenterData |
| || addingSafetySourceErrorChangedSafetyCenterData; |
| } |
| |
| /** |
| * Clears all safety source errors received so far for the given {@link UserProfileGroup}, this |
| * is useful e.g. when starting a new broadcast. |
| */ |
| void clearSafetySourceErrors(@NonNull UserProfileGroup userProfileGroup) { |
| // Loop in reverse index order to be able to remove entries while iterating. |
| for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) { |
| SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i); |
| if (userProfileGroup.contains(sourceKey.getUserId())) { |
| mSafetySourceErrors.removeAt(i); |
| } |
| } |
| } |
| |
| /** |
| * Returns the current {@link SafetyCenterData} for the given {@code packageName} and {@link |
| * UserProfileGroup}, aggregated from all the {@link SafetySourceData} set so far. |
| * |
| * <p>If a {@link SafetySourceData} was not set, the default value from the {@link |
| * SafetyCenterConfig} is used. |
| */ |
| @NonNull |
| SafetyCenterData getSafetyCenterData( |
| @NonNull String packageName, @NonNull UserProfileGroup userProfileGroup) { |
| return getSafetyCenterData( |
| mSafetyCenterConfigReader.getSafetySourcesGroups(), packageName, userProfileGroup); |
| } |
| |
| /** Marks the given {@link SafetyCenterIssueActionId} as in-flight. */ |
| void markSafetyCenterIssueActionInFlight( |
| @NonNull SafetyCenterIssueActionId safetyCenterIssueActionId) { |
| mSafetyCenterIssueActionsInFlight.put( |
| safetyCenterIssueActionId, SystemClock.elapsedRealtime()); |
| } |
| |
| /** |
| * Unmarks the given {@link SafetyCenterIssueActionId} as in-flight, logs that event to |
| * Westworld with the given {@code result} value, and returns {@code true} if the underlying |
| * {@link SafetyCenterData} changed. |
| */ |
| boolean unmarkSafetyCenterIssueActionInFlight( |
| @NonNull SafetyCenterIssueActionId safetyCenterIssueActionId, |
| @SystemEventResult int result) { |
| Long startElapsedMillis = |
| mSafetyCenterIssueActionsInFlight.remove(safetyCenterIssueActionId); |
| if (startElapsedMillis == null) { |
| Log.w( |
| TAG, |
| "Attempt to unmark unknown in-flight action: " |
| + toUserFriendlyString(safetyCenterIssueActionId)); |
| return false; |
| } |
| |
| SafetyCenterIssueKey issueKey = safetyCenterIssueActionId.getSafetyCenterIssueKey(); |
| SafetySourceIssue issue = getSafetySourceIssue(issueKey); |
| String issueTypeId = issue == null ? null : issue.getIssueTypeId(); |
| Duration duration = Duration.ofMillis(SystemClock.elapsedRealtime() - startElapsedMillis); |
| |
| mWestworldLogger.writeInlineActionSystemEvent( |
| issueKey.getSafetySourceId(), issueKey.getUserId(), issueTypeId, duration, result); |
| |
| if (issue == null || getSafetySourceIssueAction(safetyCenterIssueActionId) == null) { |
| Log.w( |
| TAG, |
| "Attempt to unmark in-flight action for a non-existent issue or action: " |
| + toUserFriendlyString(safetyCenterIssueActionId)); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Dismisses the given {@link SafetyCenterIssueKey}. |
| * |
| * <p>This method may modify the {@link SafetyCenterIssueCache}. |
| */ |
| void dismissSafetyCenterIssue(@NonNull SafetyCenterIssueKey safetyCenterIssueKey) { |
| mSafetyCenterIssueCache.dismissIssue(safetyCenterIssueKey); |
| } |
| |
| /** |
| * Returns the {@link SafetySourceIssue} associated with the given {@link SafetyCenterIssueKey}. |
| * |
| * <p>Returns {@code null} if there is no such {@link SafetySourceIssue}, or if it's been |
| * dismissed. |
| */ |
| @Nullable |
| SafetySourceIssue getSafetySourceIssue(@NonNull SafetyCenterIssueKey safetyCenterIssueKey) { |
| SafetySourceKey key = |
| SafetySourceKey.of( |
| safetyCenterIssueKey.getSafetySourceId(), safetyCenterIssueKey.getUserId()); |
| SafetySourceData safetySourceData = mSafetySourceDataForKey.get(key); |
| if (safetySourceData == null) { |
| return null; |
| } |
| List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues(); |
| |
| SafetySourceIssue targetIssue = null; |
| for (int i = 0; i < safetySourceIssues.size(); i++) { |
| SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i); |
| |
| if (safetyCenterIssueKey.getSafetySourceIssueId().equals(safetySourceIssue.getId())) { |
| targetIssue = safetySourceIssue; |
| break; |
| } |
| } |
| if (targetIssue == null) { |
| return null; |
| } |
| |
| if (mSafetyCenterIssueCache.isIssueDismissed( |
| safetyCenterIssueKey, targetIssue.getSeverityLevel())) { |
| return null; |
| } |
| |
| return targetIssue; |
| } |
| |
| /** |
| * Returns the {@link SafetySourceIssue.Action} associated with the given {@link |
| * SafetyCenterIssueActionId}. |
| * |
| * <p>Returns {@code null} if there is no associated {@link SafetySourceIssue}, or if it's been |
| * dismissed. |
| * |
| * <p>Returns {@code null} if the {@link SafetySourceIssue.Action} is currently in flight. |
| */ |
| @Nullable |
| SafetySourceIssue.Action getSafetySourceIssueAction( |
| @NonNull SafetyCenterIssueActionId safetyCenterIssueActionId) { |
| SafetySourceIssue safetySourceIssue = |
| getSafetySourceIssue(safetyCenterIssueActionId.getSafetyCenterIssueKey()); |
| |
| if (safetySourceIssue == null) { |
| return null; |
| } |
| |
| if (isInFlight(safetyCenterIssueActionId)) { |
| return null; |
| } |
| |
| List<SafetySourceIssue.Action> safetySourceIssueActions = safetySourceIssue.getActions(); |
| for (int i = 0; i < safetySourceIssueActions.size(); i++) { |
| SafetySourceIssue.Action safetySourceIssueAction = safetySourceIssueActions.get(i); |
| |
| if (safetyCenterIssueActionId |
| .getSafetySourceIssueActionId() |
| .equals(safetySourceIssueAction.getId())) { |
| return safetySourceIssueAction; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns a default {@link SafetyCenterData} object to be returned when the API is disabled. |
| */ |
| @NonNull |
| static SafetyCenterData getDefaultSafetyCenterData() { |
| return new SafetyCenterData( |
| new SafetyCenterStatus.Builder("", "") |
| .setSeverityLevel(SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN) |
| .build(), |
| emptyList(), |
| emptyList(), |
| emptyList()); |
| } |
| |
| /** Clears all {@link SafetySourceData}, errors, issues and in flight actions for all users. */ |
| void clear() { |
| mSafetySourceDataForKey.clear(); |
| mSafetySourceErrors.clear(); |
| mSafetyCenterIssueCache.clear(); |
| mSafetyCenterIssueActionsInFlight.clear(); |
| } |
| |
| /** |
| * Clears all {@link SafetySourceData}, errors, issues and in flight actions, for the given |
| * user. |
| */ |
| void clearForUser(@UserIdInt int userId) { |
| // Loop in reverse index order to be able to remove entries while iterating. |
| for (int i = mSafetySourceDataForKey.size() - 1; i >= 0; i--) { |
| SafetySourceKey sourceKey = mSafetySourceDataForKey.keyAt(i); |
| if (sourceKey.getUserId() == userId) { |
| mSafetySourceDataForKey.removeAt(i); |
| } |
| } |
| // Loop in reverse index order to be able to remove entries while iterating. |
| for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) { |
| SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i); |
| if (sourceKey.getUserId() == userId) { |
| mSafetySourceErrors.removeAt(i); |
| } |
| } |
| // Issue cache implements this itself. |
| mSafetyCenterIssueCache.clearForUser(userId); |
| // Loop in reverse index order to be able to remove entries while iterating. |
| for (int i = mSafetyCenterIssueActionsInFlight.size() - 1; i >= 0; i--) { |
| SafetyCenterIssueActionId issueActionId = mSafetyCenterIssueActionsInFlight.keyAt(i); |
| if (issueActionId.getSafetyCenterIssueKey().getUserId() == userId) { |
| mSafetyCenterIssueActionsInFlight.removeAt(i); |
| } |
| } |
| } |
| |
| /** Dumps state for debugging purposes. */ |
| void dump(@NonNull PrintWriter fout) { |
| int dataCount = mSafetySourceDataForKey.size(); |
| fout.println("SOURCE DATA (" + dataCount + ")"); |
| for (int i = 0; i < dataCount; i++) { |
| SafetySourceKey key = mSafetySourceDataForKey.keyAt(i); |
| SafetySourceData data = mSafetySourceDataForKey.valueAt(i); |
| fout.println("\t[" + i + "] " + key + " -> " + data); |
| } |
| fout.println(); |
| |
| int errorCount = mSafetySourceErrors.size(); |
| fout.println("SOURCE ERRORS (" + errorCount + ")"); |
| for (int i = 0; i < errorCount; i++) { |
| SafetySourceKey key = mSafetySourceErrors.valueAt(i); |
| fout.println("\t[" + i + "] " + key); |
| } |
| fout.println(); |
| |
| int actionInFlightCount = mSafetyCenterIssueActionsInFlight.size(); |
| fout.println("ACTIONS IN FLIGHT (" + actionInFlightCount + ")"); |
| for (int i = 0; i < actionInFlightCount; i++) { |
| SafetyCenterIssueActionId id = mSafetyCenterIssueActionsInFlight.keyAt(i); |
| long startElapsedMillis = mSafetyCenterIssueActionsInFlight.valueAt(i); |
| long durationMillis = SystemClock.elapsedRealtime() - startElapsedMillis; |
| fout.println("\t[" + i + "] " + id + "(duration=" + durationMillis + "ms)"); |
| } |
| fout.println(); |
| } |
| |
| /** |
| * Pulls the {@link PermissionStatsLog#SAFETY_STATE} atom and writes all relevant {@link |
| * PermissionStatsLog#SAFETY_SOURCE_STATE_COLLECTED} atoms for the given {@link |
| * UserProfileGroup}. |
| */ |
| void pullAndWriteAtoms( |
| @NonNull UserProfileGroup userProfileGroup, @NonNull List<StatsEvent> statsEvents) { |
| pullOverallSafetyStateAtom(userProfileGroup, statsEvents); |
| // The SAFETY_SOURCE_STATE_COLLECTED atoms are written instead of being pulled, as they do |
| // not support pull. |
| writeSafetySourceStateCollectedAtoms(userProfileGroup); |
| } |
| |
| private void pullOverallSafetyStateAtom( |
| @NonNull UserProfileGroup userProfileGroup, @NonNull List<StatsEvent> statsEvents) { |
| SafetyCenterData safetyCenterData = getSafetyCenterData("android", userProfileGroup); |
| long openIssuesCount = safetyCenterData.getIssues().size(); |
| long dismissedIssuesCount = |
| mSafetyCenterIssueCache.countActiveIssues(userProfileGroup) - openIssuesCount; |
| |
| mWestworldLogger.pullSafetyStateEvent( |
| safetyCenterData.getStatus().getSeverityLevel(), |
| openIssuesCount, |
| dismissedIssuesCount, |
| statsEvents); |
| } |
| |
| private void writeSafetySourceStateCollectedAtoms(@NonNull UserProfileGroup userProfileGroup) { |
| List<SafetySourcesGroup> safetySourcesGroups = |
| mSafetyCenterConfigReader.getSafetySourcesGroups(); |
| for (int i = 0; i < safetySourcesGroups.size(); i++) { |
| SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i); |
| List<SafetySource> safetySources = safetySourcesGroup.getSafetySources(); |
| |
| for (int j = 0; j < safetySources.size(); j++) { |
| SafetySource safetySource = safetySources.get(j); |
| |
| if (!SafetySources.isExternal(safetySource) || !safetySource.isLoggingAllowed()) { |
| continue; |
| } |
| |
| writeSafetySourceStateCollectedAtom( |
| safetySource.getId(), userProfileGroup.getProfileParentUserId(), false); |
| |
| if (!SafetySources.supportsManagedProfiles(safetySource)) { |
| continue; |
| } |
| |
| int[] managedRunningProfilesUserIds = |
| userProfileGroup.getManagedRunningProfilesUserIds(); |
| for (int k = 0; k < managedRunningProfilesUserIds.length; k++) { |
| writeSafetySourceStateCollectedAtom( |
| safetySource.getId(), managedRunningProfilesUserIds[k], true); |
| } |
| } |
| } |
| } |
| |
| private void writeSafetySourceStateCollectedAtom( |
| @NonNull String safetySourceId, @UserIdInt int userId, boolean isUserManaged) { |
| SafetySourceKey key = SafetySourceKey.of(safetySourceId, userId); |
| SafetySourceData safetySourceData = mSafetySourceDataForKey.get(key); |
| SafetySourceStatus safetySourceStatus = getSafetySourceStatus(safetySourceData); |
| List<SafetySourceIssue> safetySourceIssues = |
| safetySourceData == null ? emptyList() : safetySourceData.getIssues(); |
| int maxSeverityLevel = Integer.MIN_VALUE; |
| long openIssuesCount = 0; |
| long dismissedIssuesCount = 0; |
| for (int i = 0; i < safetySourceIssues.size(); i++) { |
| SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i); |
| SafetyCenterIssueKey safetyCenterIssueKey = |
| SafetyCenterIssueKey.newBuilder() |
| .setSafetySourceId(safetySourceId) |
| .setSafetySourceIssueId(safetySourceIssue.getId()) |
| .setUserId(userId) |
| .build(); |
| |
| if (mSafetyCenterIssueCache.isIssueDismissed( |
| safetyCenterIssueKey, safetySourceIssue.getSeverityLevel())) { |
| dismissedIssuesCount++; |
| } else { |
| openIssuesCount++; |
| maxSeverityLevel = Math.max(maxSeverityLevel, safetySourceIssue.getSeverityLevel()); |
| } |
| } |
| if (safetySourceStatus != null) { |
| maxSeverityLevel = Math.max(maxSeverityLevel, safetySourceStatus.getSeverityLevel()); |
| } |
| Integer maxSeverityOrNull = maxSeverityLevel > Integer.MIN_VALUE ? maxSeverityLevel : null; |
| |
| mWestworldLogger.writeSafetySourceStateCollected( |
| safetySourceId, |
| isUserManaged, |
| maxSeverityOrNull, |
| openIssuesCount, |
| dismissedIssuesCount); |
| } |
| |
| private boolean isInFlight(@NonNull SafetyCenterIssueActionId safetyCenterIssueActionId) { |
| return mSafetyCenterIssueActionsInFlight.containsKey(safetyCenterIssueActionId); |
| } |
| |
| /** |
| * Checks if a request to the SafetyCenter is valid, and returns whether the request should be |
| * processed. |
| */ |
| private boolean validateRequest( |
| @Nullable SafetySourceData safetySourceData, |
| @NonNull String safetySourceId, |
| @NonNull String packageName, |
| @UserIdInt int userId) { |
| ExternalSafetySource externalSafetySource = |
| mSafetyCenterConfigReader.getExternalSafetySource(safetySourceId); |
| if (externalSafetySource == null) { |
| throw new IllegalArgumentException("Unexpected safety source: " + safetySourceId); |
| } |
| |
| SafetySource safetySource = externalSafetySource.getSafetySource(); |
| |
| // TODO(b/222330089): Security: check certs? |
| if (!packageName.equals(safetySource.getPackageName())) { |
| throw new IllegalArgumentException( |
| "Unexpected package name: " |
| + packageName |
| + ", for safety source: " |
| + safetySourceId); |
| } |
| |
| // TODO(b/222327845): Security: check package is installed for user? |
| |
| if (UserUtils.isManagedProfile(userId, mContext) |
| && !SafetySources.supportsManagedProfiles(safetySource)) { |
| throw new IllegalArgumentException( |
| "Unexpected managed profile request for safety source: " + safetySourceId); |
| } |
| |
| boolean retrievingOrClearingData = safetySourceData == null; |
| if (retrievingOrClearingData) { |
| return mSafetyCenterConfigReader.isExternalSafetySourceActive(safetySourceId); |
| } |
| |
| SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); |
| |
| if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY |
| && safetySourceStatus != null) { |
| throw new IllegalArgumentException( |
| "Unexpected status for issue only safety source: " + safetySourceId); |
| } |
| |
| if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC |
| && safetySourceStatus == null) { |
| throw new IllegalArgumentException( |
| "Missing status for dynamic safety source: " + safetySourceId); |
| } |
| |
| if (safetySourceStatus != null) { |
| int sourceSeverityLevel = safetySourceStatus.getSeverityLevel(); |
| |
| if (externalSafetySource.hasEntryInRigidGroup() |
| && sourceSeverityLevel != SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED) { |
| throw new IllegalArgumentException( |
| "Safety source: " |
| + safetySourceId |
| + " is in a rigid group but specified a severity level: " |
| + sourceSeverityLevel); |
| } |
| |
| int maxSourceSeverityLevel = |
| Math.max( |
| SafetySourceData.SEVERITY_LEVEL_INFORMATION, |
| safetySource.getMaxSeverityLevel()); |
| |
| if (sourceSeverityLevel > maxSourceSeverityLevel) { |
| throw new IllegalArgumentException( |
| "Unexpected severity level: " |
| + sourceSeverityLevel |
| + ", for safety source: " |
| + safetySourceId); |
| } |
| } |
| |
| List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues(); |
| |
| for (int i = 0; i < safetySourceIssues.size(); i++) { |
| SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i); |
| int issueSeverityLevel = safetySourceIssue.getSeverityLevel(); |
| if (issueSeverityLevel > safetySource.getMaxSeverityLevel()) { |
| throw new IllegalArgumentException( |
| "Unexpected severity level: " |
| + issueSeverityLevel |
| + ", for issue in safety source: " |
| + safetySourceId); |
| } |
| |
| int issueCategory = safetySourceIssue.getIssueCategory(); |
| if (!SafetyCenterFlags.isIssueCategoryAllowedForSource(issueCategory, safetySourceId)) { |
| throw new IllegalArgumentException( |
| "Unexpected issue category: " |
| + issueCategory |
| + ", for issue in safety source: " |
| + safetySourceId); |
| } |
| } |
| |
| return mSafetyCenterConfigReader.isExternalSafetySourceActive(safetySourceId); |
| } |
| |
| private boolean processSafetyEvent( |
| @NonNull String safetySourceId, |
| @NonNull SafetyEvent safetyEvent, |
| @UserIdInt int userId, |
| boolean isError) { |
| int type = safetyEvent.getType(); |
| switch (type) { |
| case SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED: |
| String refreshBroadcastId = safetyEvent.getRefreshBroadcastId(); |
| if (refreshBroadcastId == null) { |
| Log.w( |
| TAG, |
| "Received safety event of type " |
| + safetyEvent.getType() |
| + " without a refresh broadcast id"); |
| return false; |
| } |
| return mSafetyCenterRefreshTracker.reportSourceRefreshCompleted( |
| refreshBroadcastId, safetySourceId, userId, !isError); |
| case SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED: |
| case SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED: |
| String safetySourceIssueId = safetyEvent.getSafetySourceIssueId(); |
| if (safetySourceIssueId == null) { |
| Log.w( |
| TAG, |
| "Received safety event of type " |
| + safetyEvent.getType() |
| + " without a safety source issue id"); |
| return false; |
| } |
| String safetySourceIssueActionId = safetyEvent.getSafetySourceIssueActionId(); |
| if (safetySourceIssueActionId == null) { |
| Log.w( |
| TAG, |
| "Received safety event of type " |
| + safetyEvent.getType() |
| + " without a safety source issue action id"); |
| return false; |
| } |
| SafetyCenterIssueKey safetyCenterIssueKey = |
| SafetyCenterIssueKey.newBuilder() |
| .setSafetySourceId(safetySourceId) |
| .setSafetySourceIssueId(safetySourceIssueId) |
| .setUserId(userId) |
| .build(); |
| SafetyCenterIssueActionId safetyCenterIssueActionId = |
| SafetyCenterIssueActionId.newBuilder() |
| .setSafetyCenterIssueKey(safetyCenterIssueKey) |
| .setSafetySourceIssueActionId(safetySourceIssueActionId) |
| .build(); |
| boolean success = type == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED; |
| int result = toSystemEventResult(success); |
| return unmarkSafetyCenterIssueActionInFlight(safetyCenterIssueActionId, result); |
| case SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED: |
| case SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_LOCALE_CHANGED: |
| case SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_REBOOTED: |
| return false; |
| } |
| Log.w(TAG, "Unexpected SafetyEvent.Type: " + type); |
| return false; |
| } |
| |
| @NonNull |
| private SafetyCenterData getSafetyCenterData( |
| @NonNull List<SafetySourcesGroup> safetySourcesGroups, |
| @NonNull String packageName, |
| @NonNull UserProfileGroup userProfileGroup) { |
| List<SafetyCenterIssueWithCategory> safetyCenterIssuesWithCategories = new ArrayList<>(); |
| List<SafetyCenterEntryOrGroup> safetyCenterEntryOrGroups = new ArrayList<>(); |
| List<SafetyCenterStaticEntryGroup> safetyCenterStaticEntryGroups = new ArrayList<>(); |
| SafetyCenterOverallState safetyCenterOverallState = new SafetyCenterOverallState(); |
| |
| for (int i = 0; i < safetySourcesGroups.size(); i++) { |
| SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i); |
| |
| addSafetyCenterIssues( |
| safetyCenterOverallState, |
| safetyCenterIssuesWithCategories, |
| safetySourcesGroup, |
| userProfileGroup); |
| int safetySourcesGroupType = safetySourcesGroup.getType(); |
| switch (safetySourcesGroupType) { |
| case SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_COLLAPSIBLE: |
| addSafetyCenterEntryGroup( |
| safetyCenterOverallState, |
| safetyCenterEntryOrGroups, |
| safetySourcesGroup, |
| packageName, |
| userProfileGroup); |
| break; |
| case SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_RIGID: |
| addSafetyCenterStaticEntryGroup( |
| safetyCenterOverallState, |
| safetyCenterStaticEntryGroups, |
| safetySourcesGroup, |
| packageName, |
| userProfileGroup); |
| break; |
| case SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_HIDDEN: |
| break; |
| default: |
| Log.w(TAG, "Unexpected SafetySourceGroupType: " + safetySourcesGroupType); |
| break; |
| } |
| } |
| |
| safetyCenterIssuesWithCategories.sort(SAFETY_CENTER_ISSUES_BY_SEVERITY_DESCENDING); |
| |
| List<SafetyCenterIssue> safetyCenterIssues = |
| new ArrayList<>(safetyCenterIssuesWithCategories.size()); |
| for (int i = 0; i < safetyCenterIssuesWithCategories.size(); i++) { |
| safetyCenterIssues.add(safetyCenterIssuesWithCategories.get(i).getSafetyCenterIssue()); |
| } |
| |
| int refreshStatus = mSafetyCenterRefreshTracker.getRefreshStatus(); |
| return new SafetyCenterData( |
| new SafetyCenterStatus.Builder( |
| getSafetyCenterStatusTitle( |
| safetyCenterOverallState.getOverallSeverityLevel(), |
| safetyCenterIssuesWithCategories, |
| refreshStatus, |
| safetyCenterOverallState.hasSettingsToReview()), |
| getSafetyCenterStatusSummary( |
| safetyCenterOverallState.getOverallSeverityLevel(), |
| refreshStatus, |
| safetyCenterIssues.size(), |
| safetyCenterOverallState.hasSettingsToReview())) |
| .setSeverityLevel(safetyCenterOverallState.getOverallSeverityLevel()) |
| .setRefreshStatus(refreshStatus) |
| .build(), |
| safetyCenterIssues, |
| safetyCenterEntryOrGroups, |
| safetyCenterStaticEntryGroups); |
| } |
| |
| private void addSafetyCenterIssues( |
| @NonNull SafetyCenterOverallState safetyCenterOverallState, |
| @NonNull List<SafetyCenterIssueWithCategory> safetyCenterIssuesWithCategories, |
| @NonNull SafetySourcesGroup safetySourcesGroup, |
| @NonNull UserProfileGroup userProfileGroup) { |
| List<SafetySource> safetySources = safetySourcesGroup.getSafetySources(); |
| for (int i = 0; i < safetySources.size(); i++) { |
| SafetySource safetySource = safetySources.get(i); |
| |
| if (!SafetySources.isExternal(safetySource)) { |
| continue; |
| } |
| |
| addSafetyCenterIssues( |
| safetyCenterOverallState, |
| safetyCenterIssuesWithCategories, |
| safetySource, |
| userProfileGroup.getProfileParentUserId()); |
| |
| if (!SafetySources.supportsManagedProfiles(safetySource)) { |
| continue; |
| } |
| |
| int[] managedRunningProfilesUserIds = |
| userProfileGroup.getManagedRunningProfilesUserIds(); |
| for (int j = 0; j < managedRunningProfilesUserIds.length; j++) { |
| int managedRunningProfileUserId = managedRunningProfilesUserIds[j]; |
| |
| addSafetyCenterIssues( |
| safetyCenterOverallState, |
| safetyCenterIssuesWithCategories, |
| safetySource, |
| managedRunningProfileUserId); |
| } |
| } |
| } |
| |
| private void addSafetyCenterIssues( |
| @NonNull SafetyCenterOverallState safetyCenterOverallState, |
| @NonNull List<SafetyCenterIssueWithCategory> safetyCenterIssuesWithCategories, |
| @NonNull SafetySource safetySource, |
| @UserIdInt int userId) { |
| SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId); |
| SafetySourceData safetySourceData = mSafetySourceDataForKey.get(key); |
| |
| if (safetySourceData == null) { |
| return; |
| } |
| |
| List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues(); |
| for (int i = 0; i < safetySourceIssues.size(); i++) { |
| SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i); |
| SafetyCenterIssue safetyCenterIssue = |
| toSafetyCenterIssue(safetySourceIssue, safetySource, userId); |
| |
| if (safetyCenterIssue == null) { |
| continue; |
| } |
| |
| safetyCenterOverallState.addIssueOverallSeverityLevel( |
| toSafetyCenterStatusOverallSeverityLevel(safetySourceIssue.getSeverityLevel())); |
| safetyCenterIssuesWithCategories.add( |
| SafetyCenterIssueWithCategory.create( |
| safetyCenterIssue, safetySourceIssue.getIssueCategory())); |
| } |
| } |
| |
| @Nullable |
| private SafetyCenterIssue toSafetyCenterIssue( |
| @NonNull SafetySourceIssue safetySourceIssue, |
| @NonNull SafetySource safetySource, |
| @UserIdInt int userId) { |
| SafetyCenterIssueId safetyCenterIssueId = |
| SafetyCenterIssueId.newBuilder() |
| .setSafetyCenterIssueKey( |
| SafetyCenterIssueKey.newBuilder() |
| .setSafetySourceId(safetySource.getId()) |
| .setSafetySourceIssueId(safetySourceIssue.getId()) |
| .setUserId(userId) |
| .build()) |
| .setIssueTypeId(safetySourceIssue.getIssueTypeId()) |
| .build(); |
| |
| if (mSafetyCenterIssueCache.isIssueDismissed( |
| safetyCenterIssueId.getSafetyCenterIssueKey(), |
| safetySourceIssue.getSeverityLevel())) { |
| return null; |
| } |
| |
| List<SafetySourceIssue.Action> safetySourceIssueActions = safetySourceIssue.getActions(); |
| List<SafetyCenterIssue.Action> safetyCenterIssueActions = |
| new ArrayList<>(safetySourceIssueActions.size()); |
| for (int i = 0; i < safetySourceIssueActions.size(); i++) { |
| SafetySourceIssue.Action safetySourceIssueAction = safetySourceIssueActions.get(i); |
| |
| safetyCenterIssueActions.add( |
| toSafetyCenterIssueAction( |
| safetySourceIssueAction, |
| safetyCenterIssueId.getSafetyCenterIssueKey())); |
| } |
| |
| int safetyCenterIssueSeverityLevel = |
| toSafetyCenterIssueSeverityLevel(safetySourceIssue.getSeverityLevel()); |
| return new SafetyCenterIssue.Builder( |
| SafetyCenterIds.encodeToString(safetyCenterIssueId), |
| safetySourceIssue.getTitle(), |
| safetySourceIssue.getSummary()) |
| .setSeverityLevel(safetyCenterIssueSeverityLevel) |
| .setShouldConfirmDismissal( |
| safetyCenterIssueSeverityLevel > SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK) |
| .setSubtitle(safetySourceIssue.getSubtitle()) |
| .setActions(safetyCenterIssueActions) |
| .build(); |
| } |
| |
| @NonNull |
| private SafetyCenterIssue.Action toSafetyCenterIssueAction( |
| @NonNull SafetySourceIssue.Action safetySourceIssueAction, |
| @NonNull SafetyCenterIssueKey safetyCenterIssueKey) { |
| SafetyCenterIssueActionId safetyCenterIssueActionId = |
| SafetyCenterIssueActionId.newBuilder() |
| .setSafetyCenterIssueKey(safetyCenterIssueKey) |
| .setSafetySourceIssueActionId(safetySourceIssueAction.getId()) |
| .build(); |
| return new SafetyCenterIssue.Action.Builder( |
| SafetyCenterIds.encodeToString(safetyCenterIssueActionId), |
| safetySourceIssueAction.getLabel(), |
| safetySourceIssueAction.getPendingIntent()) |
| .setSuccessMessage(safetySourceIssueAction.getSuccessMessage()) |
| .setIsInFlight(isInFlight(safetyCenterIssueActionId)) |
| .setWillResolve(safetySourceIssueAction.willResolve()) |
| .build(); |
| } |
| |
| private void addSafetyCenterEntryGroup( |
| @NonNull SafetyCenterOverallState safetyCenterOverallState, |
| @NonNull List<SafetyCenterEntryOrGroup> safetyCenterEntryOrGroups, |
| @NonNull SafetySourcesGroup safetySourcesGroup, |
| @NonNull String defaultPackageName, |
| @NonNull UserProfileGroup userProfileGroup) { |
| int groupSafetyCenterEntryLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; |
| |
| List<SafetySource> safetySources = safetySourcesGroup.getSafetySources(); |
| List<SafetyCenterEntry> entries = new ArrayList<>(safetySources.size()); |
| for (int i = 0; i < safetySources.size(); i++) { |
| SafetySource safetySource = safetySources.get(i); |
| |
| groupSafetyCenterEntryLevel = |
| mergeSafetyCenterEntrySeverityLevels( |
| groupSafetyCenterEntryLevel, |
| addSafetyCenterEntry( |
| safetyCenterOverallState, |
| entries, |
| safetySource, |
| defaultPackageName, |
| userProfileGroup.getProfileParentUserId(), |
| false, |
| false)); |
| |
| if (!SafetySources.supportsManagedProfiles(safetySource)) { |
| continue; |
| } |
| |
| int[] managedProfilesUserIds = userProfileGroup.getManagedProfilesUserIds(); |
| for (int j = 0; j < managedProfilesUserIds.length; j++) { |
| int managedProfileUserId = managedProfilesUserIds[j]; |
| boolean isManagedUserRunning = |
| userProfileGroup.isManagedUserRunning(managedProfileUserId); |
| |
| groupSafetyCenterEntryLevel = |
| mergeSafetyCenterEntrySeverityLevels( |
| groupSafetyCenterEntryLevel, |
| addSafetyCenterEntry( |
| safetyCenterOverallState, |
| entries, |
| safetySource, |
| defaultPackageName, |
| managedProfileUserId, |
| true, |
| isManagedUserRunning)); |
| } |
| } |
| |
| if (entries.size() == 0) { |
| return; |
| } |
| |
| if (entries.size() == 1) { |
| safetyCenterEntryOrGroups.add(new SafetyCenterEntryOrGroup(entries.get(0))); |
| return; |
| } |
| |
| SafetyCenterEntryGroupId safetyCenterEntryGroupId = |
| SafetyCenterEntryGroupId.newBuilder() |
| .setSafetySourcesGroupId(safetySourcesGroup.getId()) |
| .build(); |
| CharSequence groupSummary = |
| getSafetyCenterEntryGroupSummary( |
| safetySourcesGroup, groupSafetyCenterEntryLevel, entries); |
| safetyCenterEntryOrGroups.add( |
| new SafetyCenterEntryOrGroup( |
| new SafetyCenterEntryGroup.Builder( |
| SafetyCenterIds.encodeToString(safetyCenterEntryGroupId), |
| mSafetyCenterResourcesContext.getString( |
| safetySourcesGroup.getTitleResId())) |
| .setSeverityLevel(groupSafetyCenterEntryLevel) |
| .setSummary(groupSummary) |
| .setEntries(entries) |
| .setSeverityUnspecifiedIconType( |
| toGroupSeverityUnspecifiedIconType( |
| safetySourcesGroup.getStatelessIconType())) |
| .build())); |
| } |
| |
| @SafetyCenterEntry.EntrySeverityLevel |
| private static int mergeSafetyCenterEntrySeverityLevels( |
| @SafetyCenterEntry.EntrySeverityLevel int left, |
| @SafetyCenterEntry.EntrySeverityLevel int right) { |
| if (left > SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK |
| || right > SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK) { |
| return Math.max(left, right); |
| } |
| if (left == SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN |
| || right == SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN) { |
| return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN; |
| } |
| return Math.max(left, right); |
| } |
| |
| @Nullable |
| private CharSequence getSafetyCenterEntryGroupSummary( |
| @NonNull SafetySourcesGroup safetySourcesGroup, |
| @SafetyCenterEntry.EntrySeverityLevel int groupSafetyCenterEntryLevel, |
| @NonNull List<SafetyCenterEntry> entries) { |
| switch (groupSafetyCenterEntryLevel) { |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING: |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION: |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK: |
| for (int i = 0; i < entries.size(); i++) { |
| SafetyCenterEntry entry = entries.get(i); |
| |
| CharSequence entrySummary = entry.getSummary(); |
| if (entry.getSeverityLevel() != groupSafetyCenterEntryLevel |
| || entrySummary == null) { |
| continue; |
| } |
| |
| if (groupSafetyCenterEntryLevel > SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK) { |
| return entrySummary; |
| } |
| |
| SafetySourceKey key = toSafetySourceKey(entry.getId()); |
| SafetySourceData safetySourceData = mSafetySourceDataForKey.get(key); |
| boolean hasIssues = |
| safetySourceData != null && !safetySourceData.getIssues().isEmpty(); |
| |
| if (hasIssues) { |
| return entrySummary; |
| } |
| } |
| |
| return getDefaultGroupSummary(safetySourcesGroup, entries); |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED: |
| return getDefaultGroupSummary(safetySourcesGroup, entries); |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN: |
| int errorEntries = 0; |
| for (int i = 0; i < entries.size(); i++) { |
| SafetyCenterEntry entry = entries.get(i); |
| |
| SafetySourceKey key = toSafetySourceKey(entry.getId()); |
| if (mSafetySourceErrors.contains(key)) { |
| errorEntries++; |
| } |
| } |
| |
| if (errorEntries > 0) { |
| return getRefreshErrorString(errorEntries); |
| } |
| |
| return mSafetyCenterResourcesContext.getStringByName("group_unknown_summary"); |
| } |
| |
| Log.w( |
| TAG, |
| "Unexpected SafetyCenterEntry.EntrySeverityLevel for SafetyCenterEntryGroup: " |
| + groupSafetyCenterEntryLevel); |
| return getDefaultGroupSummary(safetySourcesGroup, entries); |
| } |
| |
| @Nullable |
| private CharSequence getDefaultGroupSummary( |
| @NonNull SafetySourcesGroup safetySourcesGroup, |
| @NonNull List<SafetyCenterEntry> entries) { |
| CharSequence groupSummary = |
| mSafetyCenterResourcesContext.getOptionalString( |
| safetySourcesGroup.getSummaryResId()); |
| |
| if (safetySourcesGroup.getId().equals(ANDROID_LOCK_SCREEN_SOURCES_GROUP_ID) |
| && TextUtils.isEmpty(groupSummary)) { |
| List<CharSequence> titles = new ArrayList<>(); |
| for (int i = 0; i < entries.size(); i++) { |
| titles.add(entries.get(i).getTitle()); |
| } |
| groupSummary = |
| ListFormatter.getInstance( |
| ULocale.getDefault(ULocale.Category.FORMAT), |
| ListFormatter.Type.AND, |
| ListFormatter.Width.NARROW) |
| .format(titles); |
| } |
| |
| return groupSummary; |
| } |
| |
| @SafetyCenterEntry.EntrySeverityLevel |
| private int addSafetyCenterEntry( |
| @NonNull SafetyCenterOverallState safetyCenterOverallState, |
| @NonNull List<SafetyCenterEntry> entries, |
| @NonNull SafetySource safetySource, |
| @NonNull String defaultPackageName, |
| @UserIdInt int userId, |
| boolean isUserManaged, |
| boolean isManagedUserRunning) { |
| SafetyCenterEntry safetyCenterEntry = |
| toSafetyCenterEntry( |
| safetySource, |
| defaultPackageName, |
| userId, |
| isUserManaged, |
| isManagedUserRunning); |
| if (safetyCenterEntry == null) { |
| return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; |
| } |
| |
| safetyCenterOverallState.addEntryOverallSeverityLevel( |
| entryToSafetyCenterStatusOverallSeverityLevel( |
| safetyCenterEntry.getSeverityLevel())); |
| entries.add(safetyCenterEntry); |
| |
| return safetyCenterEntry.getSeverityLevel(); |
| } |
| |
| @Nullable |
| private SafetyCenterEntry toSafetyCenterEntry( |
| @NonNull SafetySource safetySource, |
| @NonNull String defaultPackageName, |
| @UserIdInt int userId, |
| boolean isUserManaged, |
| boolean isManagedUserRunning) { |
| switch (safetySource.getType()) { |
| case SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY: |
| return null; |
| case SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC: |
| SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId); |
| SafetySourceStatus safetySourceStatus = |
| getSafetySourceStatus(mSafetySourceDataForKey.get(key)); |
| boolean defaultEntryDueToQuietMode = isUserManaged && !isManagedUserRunning; |
| if (safetySourceStatus != null && !defaultEntryDueToQuietMode) { |
| PendingIntent pendingIntent = safetySourceStatus.getPendingIntent(); |
| boolean enabled = safetySourceStatus.isEnabled(); |
| if (pendingIntent == null) { |
| pendingIntent = |
| mPendingIntentFactory.getPendingIntent( |
| safetySource.getIntentAction(), |
| safetySource.getPackageName(), |
| userId, |
| false); |
| enabled = enabled && pendingIntent != null; |
| } |
| SafetyCenterEntryId safetyCenterEntryId = |
| SafetyCenterEntryId.newBuilder() |
| .setSafetySourceId(safetySource.getId()) |
| .setUserId(userId) |
| .build(); |
| int severityUnspecifiedIconType = |
| SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION; |
| int severityLevel = |
| enabled |
| ? toSafetyCenterEntrySeverityLevel( |
| safetySourceStatus.getSeverityLevel()) |
| : SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; |
| SafetyCenterEntry.Builder builder = |
| new SafetyCenterEntry.Builder( |
| SafetyCenterIds.encodeToString(safetyCenterEntryId), |
| safetySourceStatus.getTitle()) |
| .setSeverityLevel(severityLevel) |
| .setSummary(safetySourceStatus.getSummary()) |
| .setEnabled(enabled) |
| .setSeverityUnspecifiedIconType(severityUnspecifiedIconType) |
| .setPendingIntent(pendingIntent); |
| SafetySourceStatus.IconAction iconAction = safetySourceStatus.getIconAction(); |
| if (iconAction == null) { |
| return builder.build(); |
| } |
| PendingIntent iconActionPendingIntent = |
| mPendingIntentFactory.getIconActionPendingIntent( |
| safetySource.getId(), iconAction.getPendingIntent()); |
| builder.setIconAction( |
| new SafetyCenterEntry.IconAction( |
| toSafetyCenterEntryIconActionType(iconAction.getIconType()), |
| iconActionPendingIntent)); |
| return builder.build(); |
| } |
| return toDefaultSafetyCenterEntry( |
| safetySource, |
| safetySource.getPackageName(), |
| SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN, |
| SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION, |
| userId, |
| isUserManaged, |
| isManagedUserRunning); |
| case SafetySource.SAFETY_SOURCE_TYPE_STATIC: |
| return toDefaultSafetyCenterEntry( |
| safetySource, |
| defaultPackageName, |
| SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED, |
| SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON, |
| userId, |
| isUserManaged, |
| isManagedUserRunning); |
| } |
| Log.w( |
| TAG, |
| "Unknown safety source type found in collapsible group: " + safetySource.getType()); |
| return null; |
| } |
| |
| @Nullable |
| private SafetyCenterEntry toDefaultSafetyCenterEntry( |
| @NonNull SafetySource safetySource, |
| @NonNull String packageName, |
| @SafetyCenterEntry.EntrySeverityLevel int entrySeverityLevel, |
| @SafetyCenterEntry.SeverityUnspecifiedIconType int severityUnspecifiedIconType, |
| @UserIdInt int userId, |
| boolean isUserManaged, |
| boolean isManagedUserRunning) { |
| if (SafetySources.isDefaultEntryHidden(safetySource)) { |
| return null; |
| } |
| |
| SafetyCenterEntryId safetyCenterEntryId = |
| SafetyCenterEntryId.newBuilder() |
| .setSafetySourceId(safetySource.getId()) |
| .setUserId(userId) |
| .build(); |
| boolean isQuietModeEnabled = isUserManaged && !isManagedUserRunning; |
| PendingIntent pendingIntent = |
| mPendingIntentFactory.getPendingIntent( |
| safetySource.getIntentAction(), packageName, userId, isQuietModeEnabled); |
| boolean enabled = |
| pendingIntent != null && !SafetySources.isDefaultEntryDisabled(safetySource); |
| CharSequence title = |
| isUserManaged |
| ? DevicePolicyResources.getSafetySourceWorkString( |
| mSafetyCenterResourcesContext, |
| safetySource.getId(), |
| safetySource.getTitleForWorkResId()) |
| : mSafetyCenterResourcesContext.getString(safetySource.getTitleResId()); |
| CharSequence summary = |
| mSafetySourceErrors.contains(SafetySourceKey.of(safetySource.getId(), userId)) |
| ? getRefreshErrorString(1) |
| : mSafetyCenterResourcesContext.getOptionalString( |
| safetySource.getSummaryResId()); |
| if (isQuietModeEnabled) { |
| enabled = false; |
| summary = |
| DevicePolicyResources.getWorkProfilePausedString(mSafetyCenterResourcesContext); |
| } |
| return new SafetyCenterEntry.Builder( |
| SafetyCenterIds.encodeToString(safetyCenterEntryId), title) |
| .setSeverityLevel(entrySeverityLevel) |
| .setSummary(summary) |
| .setEnabled(enabled) |
| .setPendingIntent(pendingIntent) |
| .setSeverityUnspecifiedIconType(severityUnspecifiedIconType) |
| .build(); |
| } |
| |
| private void addSafetyCenterStaticEntryGroup( |
| @NonNull SafetyCenterOverallState safetyCenterOverallState, |
| @NonNull List<SafetyCenterStaticEntryGroup> safetyCenterStaticEntryGroups, |
| @NonNull SafetySourcesGroup safetySourcesGroup, |
| @NonNull String defaultPackageName, |
| @NonNull UserProfileGroup userProfileGroup) { |
| List<SafetySource> safetySources = safetySourcesGroup.getSafetySources(); |
| List<SafetyCenterStaticEntry> staticEntries = new ArrayList<>(safetySources.size()); |
| for (int i = 0; i < safetySources.size(); i++) { |
| SafetySource safetySource = safetySources.get(i); |
| |
| addSafetyCenterStaticEntry( |
| safetyCenterOverallState, |
| staticEntries, |
| safetySource, |
| defaultPackageName, |
| userProfileGroup.getProfileParentUserId(), |
| false, |
| false); |
| |
| if (!SafetySources.supportsManagedProfiles(safetySource)) { |
| continue; |
| } |
| |
| int[] managedProfilesUserIds = userProfileGroup.getManagedProfilesUserIds(); |
| for (int j = 0; j < managedProfilesUserIds.length; j++) { |
| int managedProfileUserId = managedProfilesUserIds[j]; |
| boolean isManagedUserRunning = |
| userProfileGroup.isManagedUserRunning(managedProfileUserId); |
| |
| addSafetyCenterStaticEntry( |
| safetyCenterOverallState, |
| staticEntries, |
| safetySource, |
| defaultPackageName, |
| managedProfileUserId, |
| true, |
| isManagedUserRunning); |
| } |
| } |
| |
| safetyCenterStaticEntryGroups.add( |
| new SafetyCenterStaticEntryGroup( |
| mSafetyCenterResourcesContext.getString(safetySourcesGroup.getTitleResId()), |
| staticEntries)); |
| } |
| |
| private void addSafetyCenterStaticEntry( |
| @NonNull SafetyCenterOverallState safetyCenterOverallState, |
| @NonNull List<SafetyCenterStaticEntry> staticEntries, |
| @NonNull SafetySource safetySource, |
| @NonNull String defaultPackageName, |
| @UserIdInt int userId, |
| boolean isUserManaged, |
| boolean isManagedUserRunning) { |
| SafetyCenterStaticEntry staticEntry = |
| toSafetyCenterStaticEntry( |
| safetySource, |
| defaultPackageName, |
| userId, |
| isUserManaged, |
| isManagedUserRunning); |
| if (staticEntry == null) { |
| return; |
| } |
| boolean isQuietModeEnabled = isUserManaged && !isManagedUserRunning; |
| boolean hasError = |
| mSafetySourceErrors.contains(SafetySourceKey.of(safetySource.getId(), userId)); |
| if (isQuietModeEnabled || hasError) { |
| safetyCenterOverallState.addEntryOverallSeverityLevel( |
| SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN); |
| } |
| staticEntries.add(staticEntry); |
| } |
| |
| @Nullable |
| private SafetyCenterStaticEntry toSafetyCenterStaticEntry( |
| @NonNull SafetySource safetySource, |
| @NonNull String defaultPackageName, |
| @UserIdInt int userId, |
| boolean isUserManaged, |
| boolean isManagedUserRunning) { |
| switch (safetySource.getType()) { |
| case SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY: |
| return null; |
| case SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC: |
| SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId); |
| SafetySourceStatus safetySourceStatus = |
| getSafetySourceStatus(mSafetySourceDataForKey.get(key)); |
| boolean defaultEntryDueToQuietMode = isUserManaged && !isManagedUserRunning; |
| if (safetySourceStatus != null && !defaultEntryDueToQuietMode) { |
| PendingIntent pendingIntent = safetySourceStatus.getPendingIntent(); |
| if (pendingIntent == null) { |
| // TODO(b/222838784): Decide strategy for static entries when the intent is |
| // null. |
| return null; |
| } |
| return new SafetyCenterStaticEntry.Builder(safetySourceStatus.getTitle()) |
| .setSummary(safetySourceStatus.getSummary()) |
| .setPendingIntent(pendingIntent) |
| .build(); |
| } |
| return toDefaultSafetyCenterStaticEntry( |
| safetySource, |
| safetySource.getPackageName(), |
| userId, |
| isUserManaged, |
| isManagedUserRunning); |
| case SafetySource.SAFETY_SOURCE_TYPE_STATIC: |
| return toDefaultSafetyCenterStaticEntry( |
| safetySource, |
| defaultPackageName, |
| userId, |
| isUserManaged, |
| isManagedUserRunning); |
| } |
| Log.w(TAG, "Unknown safety source type found in rigid group: " + safetySource.getType()); |
| return null; |
| } |
| |
| @Nullable |
| private SafetyCenterStaticEntry toDefaultSafetyCenterStaticEntry( |
| @NonNull SafetySource safetySource, |
| @NonNull String packageName, |
| @UserIdInt int userId, |
| boolean isUserManaged, |
| boolean isManagedUserRunning) { |
| if (SafetySources.isDefaultEntryHidden(safetySource)) { |
| return null; |
| } |
| boolean isQuietModeEnabled = isUserManaged && !isManagedUserRunning; |
| PendingIntent pendingIntent = |
| mPendingIntentFactory.getPendingIntent( |
| safetySource.getIntentAction(), packageName, userId, isQuietModeEnabled); |
| |
| if (pendingIntent == null) { |
| // TODO(b/222838784): Decide strategy for static entries when the intent is null. |
| return null; |
| } |
| |
| CharSequence title = |
| isUserManaged |
| ? DevicePolicyResources.getSafetySourceWorkString( |
| mSafetyCenterResourcesContext, |
| safetySource.getId(), |
| safetySource.getTitleForWorkResId()) |
| : mSafetyCenterResourcesContext.getString(safetySource.getTitleResId()); |
| CharSequence summary = |
| mSafetySourceErrors.contains(SafetySourceKey.of(safetySource.getId(), userId)) |
| ? getRefreshErrorString(1) |
| : mSafetyCenterResourcesContext.getOptionalString( |
| safetySource.getSummaryResId()); |
| if (isQuietModeEnabled) { |
| summary = |
| DevicePolicyResources.getWorkProfilePausedString(mSafetyCenterResourcesContext); |
| } |
| return new SafetyCenterStaticEntry.Builder(title) |
| .setSummary(summary) |
| .setPendingIntent(pendingIntent) |
| .build(); |
| } |
| |
| @Nullable |
| private static SafetySourceStatus getSafetySourceStatus( |
| @Nullable SafetySourceData safetySourceData) { |
| if (safetySourceData == null) { |
| return null; |
| } |
| |
| return safetySourceData.getStatus(); |
| } |
| |
| @SafetyCenterStatus.OverallSeverityLevel |
| private static int toSafetyCenterStatusOverallSeverityLevel( |
| @SafetySourceData.SeverityLevel int safetySourceSeverityLevel) { |
| switch (safetySourceSeverityLevel) { |
| case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: |
| case SafetySourceData.SEVERITY_LEVEL_INFORMATION: |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK; |
| case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION; |
| case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING; |
| } |
| |
| Log.w(TAG, "Unexpected SafetySourceData.SeverityLevel: " + safetySourceSeverityLevel); |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; |
| } |
| |
| @SafetyCenterStatus.OverallSeverityLevel |
| private static int entryToSafetyCenterStatusOverallSeverityLevel( |
| @SafetyCenterEntry.EntrySeverityLevel int safetyCenterEntrySeverityLevel) { |
| switch (safetyCenterEntrySeverityLevel) { |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN: |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED: |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK: |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK; |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION: |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION; |
| case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING: |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING; |
| } |
| |
| Log.w( |
| TAG, |
| "Unexpected SafetyCenterEntry.EntrySeverityLevel: " |
| + safetyCenterEntrySeverityLevel); |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; |
| } |
| |
| @SafetyCenterEntry.EntrySeverityLevel |
| private static int toSafetyCenterEntrySeverityLevel( |
| @SafetySourceData.SeverityLevel int safetySourceSeverityLevel) { |
| switch (safetySourceSeverityLevel) { |
| case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: |
| return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; |
| case SafetySourceData.SEVERITY_LEVEL_INFORMATION: |
| return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK; |
| case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: |
| return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION; |
| case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: |
| return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING; |
| } |
| |
| Log.w( |
| TAG, |
| "Unexpected SafetySourceData.SeverityLevel in SafetySourceStatus: " |
| + safetySourceSeverityLevel); |
| return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN; |
| } |
| |
| @SafetyCenterIssue.IssueSeverityLevel |
| private static int toSafetyCenterIssueSeverityLevel( |
| @SafetySourceData.SeverityLevel int safetySourceIssueSeverityLevel) { |
| switch (safetySourceIssueSeverityLevel) { |
| case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: |
| Log.w( |
| TAG, |
| "Unexpected use of SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED in " |
| + "SafetySourceIssue"); |
| return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK; |
| case SafetySourceData.SEVERITY_LEVEL_INFORMATION: |
| return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK; |
| case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: |
| return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION; |
| case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: |
| return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING; |
| } |
| |
| Log.w( |
| TAG, |
| "Unexpected SafetySourceData.SeverityLevel in SafetySourceIssue: " |
| + safetySourceIssueSeverityLevel); |
| return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK; |
| } |
| |
| @SafetyCenterEntry.SeverityUnspecifiedIconType |
| private static int toGroupSeverityUnspecifiedIconType( |
| @SafetySourcesGroup.StatelessIconType int statelessIconType) { |
| switch (statelessIconType) { |
| case SafetySourcesGroup.STATELESS_ICON_TYPE_NONE: |
| return SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON; |
| case SafetySourcesGroup.STATELESS_ICON_TYPE_PRIVACY: |
| return SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_PRIVACY; |
| } |
| |
| Log.w(TAG, "Unexpected SafetySourcesGroup.StatelessIconType: " + statelessIconType); |
| return SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON; |
| } |
| |
| @SafetyCenterEntry.IconAction.IconActionType |
| private static int toSafetyCenterEntryIconActionType( |
| @SafetySourceStatus.IconAction.IconType int safetySourceIconActionType) { |
| switch (safetySourceIconActionType) { |
| case SafetySourceStatus.IconAction.ICON_TYPE_GEAR: |
| return SafetyCenterEntry.IconAction.ICON_ACTION_TYPE_GEAR; |
| case SafetySourceStatus.IconAction.ICON_TYPE_INFO: |
| return SafetyCenterEntry.IconAction.ICON_ACTION_TYPE_INFO; |
| } |
| |
| Log.w( |
| TAG, |
| "Unexpected SafetySourceStatus.IconAction.IconActionType: " |
| + safetySourceIconActionType); |
| return SafetyCenterEntry.IconAction.ICON_ACTION_TYPE_INFO; |
| } |
| |
| @NonNull |
| private String getSafetyCenterStatusTitle( |
| @SafetyCenterStatus.OverallSeverityLevel int overallSeverityLevel, |
| @NonNull List<SafetyCenterIssueWithCategory> safetyCenterIssuesWithCategories, |
| @SafetyCenterStatus.RefreshStatus int refreshStatus, |
| boolean hasSettingsToReview) { |
| boolean overallSeverityUnknown = |
| overallSeverityLevel == SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; |
| String refreshStatusTitle = |
| getSafetyCenterRefreshStatusTitle(refreshStatus, overallSeverityUnknown); |
| if (refreshStatusTitle != null) { |
| return refreshStatusTitle; |
| } |
| switch (overallSeverityLevel) { |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN: |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK: |
| if (hasSettingsToReview) { |
| return mSafetyCenterResourcesContext.getStringByName( |
| "overall_severity_level_ok_review_title"); |
| } |
| return mSafetyCenterResourcesContext.getStringByName( |
| "overall_severity_level_ok_title"); |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION: |
| return getStatusTitleFromIssueCategories( |
| safetyCenterIssuesWithCategories, |
| "overall_severity_level_device_recommendation_title", |
| "overall_severity_level_account_recommendation_title", |
| "overall_severity_level_safety_recommendation_title"); |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING: |
| return getStatusTitleFromIssueCategories( |
| safetyCenterIssuesWithCategories, |
| "overall_severity_level_critical_device_warning_title", |
| "overall_severity_level_critical_account_warning_title", |
| "overall_severity_level_critical_safety_warning_title"); |
| } |
| |
| Log.w(TAG, "Unexpected SafetyCenterStatus.OverallSeverityLevel: " + overallSeverityLevel); |
| return ""; |
| } |
| |
| @NonNull |
| private String getStatusTitleFromIssueCategories( |
| @NonNull List<SafetyCenterIssueWithCategory> safetyCenterIssuesWithCategories, |
| @NonNull String deviceResourceName, |
| @NonNull String accountResourceName, |
| @NonNull String generalResourceName) { |
| String generalString = mSafetyCenterResourcesContext.getStringByName(generalResourceName); |
| if (safetyCenterIssuesWithCategories.isEmpty()) { |
| Log.w(TAG, "No safety center issues found in a non-green status"); |
| return generalString; |
| } |
| int issueCategory = safetyCenterIssuesWithCategories.get(0).getSafetyCenterIssueCategory(); |
| switch (issueCategory) { |
| case SafetySourceIssue.ISSUE_CATEGORY_DEVICE: |
| return mSafetyCenterResourcesContext.getStringByName(deviceResourceName); |
| case SafetySourceIssue.ISSUE_CATEGORY_ACCOUNT: |
| return mSafetyCenterResourcesContext.getStringByName(accountResourceName); |
| case SafetySourceIssue.ISSUE_CATEGORY_GENERAL: |
| return generalString; |
| } |
| |
| Log.w(TAG, "Unexpected SafetySourceIssue.IssueCategory: " + issueCategory); |
| return generalString; |
| } |
| |
| @NonNull |
| private String getSafetyCenterStatusSummary( |
| @SafetyCenterStatus.OverallSeverityLevel int overallSeverityLevel, |
| @SafetyCenterStatus.RefreshStatus int refreshStatus, |
| int numberOfIssues, |
| boolean hasSettingsToReview) { |
| String refreshStatusSummary = getSafetyCenterRefreshStatusSummary(refreshStatus); |
| if (refreshStatusSummary != null) { |
| return refreshStatusSummary; |
| } |
| switch (overallSeverityLevel) { |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN: |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK: |
| if (numberOfIssues == 0) { |
| if (hasSettingsToReview) { |
| return mSafetyCenterResourcesContext.getStringByName( |
| "overall_severity_level_ok_review_summary"); |
| } |
| return mSafetyCenterResourcesContext.getStringByName( |
| "overall_severity_level_ok_summary"); |
| } |
| // Fall through. |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION: |
| case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING: |
| return getIcuPluralsString("overall_severity_n_alerts_summary", numberOfIssues); |
| } |
| |
| Log.w(TAG, "Unexpected SafetyCenterStatus.OverallSeverityLevel: " + overallSeverityLevel); |
| return ""; |
| } |
| |
| @NonNull |
| private String getRefreshErrorString(int numberOfErrorEntries) { |
| return getIcuPluralsString("refresh_error", numberOfErrorEntries); |
| } |
| |
| @NonNull |
| private String getIcuPluralsString(String name, int count, @NonNull Object... formatArgs) { |
| MessageFormat messageFormat = |
| new MessageFormat( |
| mSafetyCenterResourcesContext.getStringByName(name, formatArgs), |
| Locale.getDefault()); |
| ArrayMap<String, Object> arguments = new ArrayMap<>(); |
| arguments.put("count", count); |
| return messageFormat.format(arguments); |
| } |
| |
| @Nullable |
| private String getSafetyCenterRefreshStatusTitle( |
| @SafetyCenterStatus.RefreshStatus int refreshStatus, boolean overallSeverityUnknown) { |
| switch (refreshStatus) { |
| case SafetyCenterStatus.REFRESH_STATUS_NONE: |
| return null; |
| case SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS: |
| if (!overallSeverityUnknown) { |
| return null; |
| } |
| // Fall through. |
| case SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS: |
| return mSafetyCenterResourcesContext.getStringByName("scanning_title"); |
| } |
| |
| Log.w(TAG, "Unexpected SafetyCenterStatus.RefreshStatus: " + refreshStatus); |
| return null; |
| } |
| |
| @Nullable |
| private String getSafetyCenterRefreshStatusSummary( |
| @SafetyCenterStatus.RefreshStatus int refreshStatus) { |
| switch (refreshStatus) { |
| case SafetyCenterStatus.REFRESH_STATUS_NONE: |
| return null; |
| case SafetyCenterStatus.REFRESH_STATUS_DATA_FETCH_IN_PROGRESS: |
| case SafetyCenterStatus.REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS: |
| return mSafetyCenterResourcesContext.getStringByName("loading_summary"); |
| } |
| |
| Log.w(TAG, "Unexpected SafetyCenterStatus.RefreshStatus: " + refreshStatus); |
| return null; |
| } |
| |
| @NonNull |
| private static SafetySourceKey toSafetySourceKey(@NonNull String safetyCenterEntryIdString) { |
| SafetyCenterEntryId id = SafetyCenterIds.entryIdFromString(safetyCenterEntryIdString); |
| return SafetySourceKey.of(id.getSafetySourceId(), id.getUserId()); |
| } |
| |
| /** Wrapper that encapsulates both {@link SafetyCenterIssue} and its category. */ |
| private static final class SafetyCenterIssueWithCategory { |
| @NonNull private final SafetyCenterIssue mSafetyCenterIssue; |
| @SafetySourceIssue.IssueCategory private final int mSafetyCenterIssueCategory; |
| |
| private SafetyCenterIssueWithCategory( |
| @NonNull SafetyCenterIssue safetyCenterIssue, |
| @SafetySourceIssue.IssueCategory int safetyCenterIssueCategory) { |
| this.mSafetyCenterIssue = safetyCenterIssue; |
| this.mSafetyCenterIssueCategory = safetyCenterIssueCategory; |
| } |
| |
| @NonNull |
| private SafetyCenterIssue getSafetyCenterIssue() { |
| return mSafetyCenterIssue; |
| } |
| |
| @SafetySourceIssue.IssueCategory |
| private int getSafetyCenterIssueCategory() { |
| return mSafetyCenterIssueCategory; |
| } |
| |
| private static SafetyCenterIssueWithCategory create( |
| @NonNull SafetyCenterIssue safetyCenterIssue, |
| @SafetySourceIssue.IssueCategory int safetyCenterIssueCategory) { |
| return new SafetyCenterIssueWithCategory(safetyCenterIssue, safetyCenterIssueCategory); |
| } |
| } |
| |
| /** A comparator to order {@link SafetyCenterIssueWithCategory} by severity level descending. */ |
| private static final class SafetyCenterIssuesBySeverityDescending |
| implements Comparator<SafetyCenterIssueWithCategory> { |
| |
| private SafetyCenterIssuesBySeverityDescending() {} |
| |
| @Override |
| public int compare( |
| @NonNull SafetyCenterIssueWithCategory left, |
| @NonNull SafetyCenterIssueWithCategory right) { |
| return Integer.compare( |
| right.getSafetyCenterIssue().getSeverityLevel(), |
| left.getSafetyCenterIssue().getSeverityLevel()); |
| } |
| } |
| |
| /** |
| * An internal mutable class to keep track of the overall {@link SafetyCenterStatus} severity |
| * level and whether the list of entries provided requires attention. |
| */ |
| private static final class SafetyCenterOverallState { |
| |
| @SafetyCenterStatus.OverallSeverityLevel |
| private int mIssuesOverallSeverityLevel = SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK; |
| |
| @SafetyCenterStatus.OverallSeverityLevel |
| private int mEntriesOverallSeverityLevel = SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK; |
| |
| /** |
| * Adds a {@link SafetyCenterStatus.OverallSeverityLevel} computed from an issue. |
| * |
| * <p>The {@code overallSeverityLevel} provided cannot be {@link |
| * SafetyCenterStatus#OVERALL_SEVERITY_LEVEL_UNKNOWN}. If the data for an issue is not |
| * provided yet, this will be reflected when calling {@link |
| * #addEntryOverallSeverityLevel(int)}. The exception to that are issue-only safety sources |
| * but since they do not have user-visible entries they do not affect whether the overall |
| * status is unknown. |
| */ |
| private void addIssueOverallSeverityLevel( |
| @SafetyCenterStatus.OverallSeverityLevel int issueOverallSeverityLevel) { |
| if (issueOverallSeverityLevel == SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN) { |
| return; |
| } |
| mIssuesOverallSeverityLevel = |
| mergeOverallSeverityLevels( |
| mIssuesOverallSeverityLevel, issueOverallSeverityLevel); |
| } |
| |
| /** |
| * Adds a {@link SafetyCenterStatus.OverallSeverityLevel} computed from an entry. |
| * |
| * <p>Entries may be unknown (e.g. due to an error or no data provided yet). In this case, |
| * the overall status will be marked as unknown if there are no recommendations or critical |
| * issues. |
| */ |
| private void addEntryOverallSeverityLevel( |
| @SafetyCenterStatus.OverallSeverityLevel int entryOverallSeverityLevel) { |
| mEntriesOverallSeverityLevel = |
| mergeOverallSeverityLevels( |
| mEntriesOverallSeverityLevel, entryOverallSeverityLevel); |
| } |
| |
| /** |
| * Returns the {@link SafetyCenterStatus.OverallSeverityLevel} computed. |
| * |
| * <p>Returns {@link SafetyCenterStatus#OVERALL_SEVERITY_LEVEL_UNKNOWN} if any entry is |
| * unknown / has errored-out and there are no recommendations or critical issues. |
| * |
| * <p>Otherwise, this is computed based on the maximum severity level of issues. |
| */ |
| @SafetyCenterStatus.OverallSeverityLevel |
| private int getOverallSeverityLevel() { |
| if (mEntriesOverallSeverityLevel == SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN |
| && mIssuesOverallSeverityLevel |
| <= SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK) { |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; |
| } |
| return mIssuesOverallSeverityLevel; |
| } |
| |
| /** |
| * Returns whether there are settings to review (i.e. at least one entry has a more severe |
| * status than the overall status, or if any entry is not yet known / has errored-out). |
| */ |
| private boolean hasSettingsToReview() { |
| return mEntriesOverallSeverityLevel == SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN |
| || mEntriesOverallSeverityLevel > mIssuesOverallSeverityLevel; |
| } |
| |
| @SafetyCenterStatus.OverallSeverityLevel |
| private static int mergeOverallSeverityLevels( |
| @SafetyCenterStatus.OverallSeverityLevel int left, |
| @SafetyCenterStatus.OverallSeverityLevel int right) { |
| if (left == SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN |
| || right == SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN) { |
| return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; |
| } |
| return Math.max(left, right); |
| } |
| } |
| } |