/*
* Copyright (C) 2023 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.data;
import static com.android.safetycenter.data.SafetyCenterIssueDeduplicator.DeduplicationInfo;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import android.annotation.UserIdInt;
import android.content.Context;
import android.safetycenter.SafetySourceData;
import android.safetycenter.SafetySourceIssue;
import android.safetycenter.config.SafetySource;
import android.safetycenter.config.SafetySourcesGroup;
import android.util.SparseArray;
import com.android.modules.utils.build.SdkLevel;
import com.android.safetycenter.SafetyCenterConfigReader;
import com.android.safetycenter.SafetySourceIssueInfo;
import com.android.safetycenter.SafetySourceKey;
import com.android.safetycenter.SafetySources;
import com.android.safetycenter.UserProfileGroup;
import com.android.safetycenter.UserProfileGroup.ProfileType;
import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Contains issue related data.
*
*
Responsible for generating lists of issues and deduplication of issues.
*/
@NotThreadSafe
final class SafetyCenterIssueRepository {
private static final SafetySourceIssuesInfoBySeverityDescending
SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING =
new SafetySourceIssuesInfoBySeverityDescending();
private static final DeduplicationInfo EMPTY_DEDUP_INFO =
new DeduplicationInfo(emptyList(), emptyList(), emptyMap());
private final Context mContext;
private final SafetySourceDataRepository mSafetySourceDataRepository;
private final SafetyCenterConfigReader mSafetyCenterConfigReader;
private final SafetyCenterIssueDismissalRepository mSafetyCenterIssueDismissalRepository;
private final SafetyCenterIssueDeduplicator mSafetyCenterIssueDeduplicator;
private final SparseArray mUserIdToDedupInfo = new SparseArray<>();
SafetyCenterIssueRepository(
Context context,
SafetySourceDataRepository safetySourceDataRepository,
SafetyCenterConfigReader safetyCenterConfigReader,
SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository,
SafetyCenterIssueDeduplicator safetyCenterIssueDeduplicator) {
mContext = context;
mSafetySourceDataRepository = safetySourceDataRepository;
mSafetyCenterConfigReader = safetyCenterConfigReader;
mSafetyCenterIssueDismissalRepository = safetyCenterIssueDismissalRepository;
mSafetyCenterIssueDeduplicator = safetyCenterIssueDeduplicator;
}
/**
* Updates the class as per the current state of issues. Should be called after any state update
* that can affect issues.
*/
void updateIssues(@UserIdInt int userId) {
updateIssues(userId, UserProfileGroup.getProfileTypeOfUser(userId, mContext));
}
private void updateIssues(@UserIdInt int userId, @ProfileType int profileType) {
List issues =
getAllStoredIssuesFromRawSourceData(userId, profileType);
issues.sort(SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING);
mUserIdToDedupInfo.put(userId, produceDedupInfo(issues));
}
private DeduplicationInfo produceDedupInfo(List issues) {
if (SdkLevel.isAtLeastU()) {
return mSafetyCenterIssueDeduplicator.deduplicateIssues(issues);
}
return new DeduplicationInfo(issues, emptyList(), emptyMap());
}
/**
* Fetches a list of issues related to the given {@link UserProfileGroup}.
*
* Issues in the list are sorted in descending order and deduplicated (if applicable, only on
* Android U+).
*
*
Only includes issues related to active/running {@code userId}s in the given {@link
* UserProfileGroup}.
*/
List getIssuesDedupedSortedDescFor(UserProfileGroup userProfileGroup) {
List issuesInfo = getIssuesFor(userProfileGroup);
issuesInfo.sort(SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING);
return issuesInfo;
}
/**
* Counts the total number of issues from loggable sources, in the given {@link
* UserProfileGroup}.
*
* Only includes issues related to active/running {@code userId}s in the given {@link
* UserProfileGroup}.
*/
int countLoggableIssuesFor(UserProfileGroup userProfileGroup) {
List relevantIssues = getIssuesFor(userProfileGroup);
int issueCount = 0;
for (int i = 0; i < relevantIssues.size(); i++) {
SafetySourceIssueInfo safetySourceIssueInfo = relevantIssues.get(i);
if (SafetySources.isLoggable(safetySourceIssueInfo.getSafetySource())) {
issueCount++;
}
}
return issueCount;
}
/** Gets a list of all issues for the given {@code userId}. */
List getIssuesForUser(@UserIdInt int userId) {
return filterOutHiddenIssues(
mUserIdToDedupInfo.get(userId, EMPTY_DEDUP_INFO).getDeduplicatedIssues());
}
/**
* Returns a set of {@link SafetySourcesGroup} IDs that the given {@link SafetyCenterIssueKey}
* is mapped to, or an empty list if no such mapping is configured.
*/
Set getGroupMappingFor(SafetyCenterIssueKey issueKey) {
return mUserIdToDedupInfo
.get(issueKey.getUserId(), EMPTY_DEDUP_INFO)
.getIssueToGroupMapping()
.getOrDefault(issueKey, emptySet());
}
/**
* Returns the list of issues for the given {@code userId} which were removed from the given
* list of issues in the most recent {@link SafetyCenterIssueDeduplicator#deduplicateIssues}
* call. These issues were removed because they were duplicates of other issues.
*
* If this method is called before any calls to {@link
* SafetyCenterIssueDeduplicator#deduplicateIssues} then an empty list is returned.
*/
List getLatestDuplicates(@UserIdInt int userId) {
return mUserIdToDedupInfo.get(userId, EMPTY_DEDUP_INFO).getFilteredOutDuplicateIssues();
}
private List filterOutHiddenIssues(List issues) {
List result = new ArrayList<>();
for (int i = 0; i < issues.size(); i++) {
SafetySourceIssueInfo issueInfo = issues.get(i);
if (!mSafetyCenterIssueDismissalRepository.isIssueHidden(
issueInfo.getSafetyCenterIssueKey())) {
result.add(issueInfo);
}
}
return result;
}
private List getAllStoredIssuesFromRawSourceData(
@UserIdInt int userId, @ProfileType int profileType) {
List allIssuesInfo = new ArrayList<>();
List safetySourcesGroups =
mSafetyCenterConfigReader.getSafetySourcesGroups();
for (int j = 0; j < safetySourcesGroups.size(); j++) {
addSafetySourceIssuesInfo(
allIssuesInfo, safetySourcesGroups.get(j), userId, profileType);
}
return allIssuesInfo;
}
private void addSafetySourceIssuesInfo(
List issuesInfo,
SafetySourcesGroup safetySourcesGroup,
@UserIdInt int userId,
@ProfileType int profileType) {
List safetySources = safetySourcesGroup.getSafetySources();
for (int i = 0; i < safetySources.size(); i++) {
SafetySource safetySource = safetySources.get(i);
if (!SafetySources.isExternal(safetySource)) {
continue;
}
if (!SafetySources.supportsProfileType(safetySource, profileType)) {
continue;
}
addSafetySourceIssuesInfo(issuesInfo, safetySource, safetySourcesGroup, userId);
}
}
private void addSafetySourceIssuesInfo(
List issuesInfo,
SafetySource safetySource,
SafetySourcesGroup safetySourcesGroup,
@UserIdInt int userId) {
SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId);
SafetySourceData safetySourceData = mSafetySourceDataRepository.getSafetySourceData(key);
if (safetySourceData == null) {
return;
}
List safetySourceIssues = safetySourceData.getIssues();
for (int i = 0; i < safetySourceIssues.size(); i++) {
SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
SafetySourceIssueInfo safetySourceIssueInfo =
new SafetySourceIssueInfo(
safetySourceIssue, safetySource, safetySourcesGroup, userId);
issuesInfo.add(safetySourceIssueInfo);
}
}
/**
* Only includes issues related to active/running {@code userId}s in the given {@link
* UserProfileGroup}.
*/
private List getIssuesFor(UserProfileGroup userProfileGroup) {
List issues = new ArrayList<>();
int[] allRunningProfileUserIds = userProfileGroup.getAllRunningProfilesUserIds();
for (int i = 0; i < allRunningProfileUserIds.length; i++) {
issues.addAll(getIssuesForUser(allRunningProfileUserIds[i]));
}
return issues;
}
/** A comparator to order {@link SafetySourceIssueInfo} by severity level descending. */
private static final class SafetySourceIssuesInfoBySeverityDescending
implements Comparator {
private SafetySourceIssuesInfoBySeverityDescending() {}
@Override
public int compare(SafetySourceIssueInfo left, SafetySourceIssueInfo right) {
return Integer.compare(
right.getSafetySourceIssue().getSeverityLevel(),
left.getSafetySourceIssue().getSeverityLevel());
}
}
/** Dumps state for debugging purposes. */
void dump(PrintWriter fout) {
fout.println("ISSUE REPOSITORY");
for (int i = 0; i < mUserIdToDedupInfo.size(); i++) {
fout.println();
fout.println("\tUSER ID=" + mUserIdToDedupInfo.keyAt(i));
fout.println("\tDEDUPLICATION INFO=" + mUserIdToDedupInfo.valueAt(i));
}
fout.println();
}
/** Clears all the data from the repository. */
void clear() {
mUserIdToDedupInfo.clear();
}
/** Clears all data related to the given {@code userId}. */
void clearForUser(@UserIdInt int userId) {
mUserIdToDedupInfo.delete(userId);
}
}