1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.safetycenter.data;
18 
19 import static com.android.safetycenter.data.SafetyCenterIssueDeduplicator.DeduplicationInfo;
20 
21 import static java.util.Collections.emptyList;
22 import static java.util.Collections.emptyMap;
23 import static java.util.Collections.emptySet;
24 
25 import android.annotation.UserIdInt;
26 import android.content.Context;
27 import android.safetycenter.SafetySourceData;
28 import android.safetycenter.SafetySourceIssue;
29 import android.safetycenter.config.SafetySource;
30 import android.safetycenter.config.SafetySourcesGroup;
31 import android.util.SparseArray;
32 
33 import com.android.modules.utils.build.SdkLevel;
34 import com.android.safetycenter.SafetyCenterConfigReader;
35 import com.android.safetycenter.SafetySourceIssueInfo;
36 import com.android.safetycenter.SafetySourceKey;
37 import com.android.safetycenter.SafetySources;
38 import com.android.safetycenter.UserProfileGroup;
39 import com.android.safetycenter.UserProfileGroup.ProfileType;
40 import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
41 
42 import java.io.PrintWriter;
43 import java.util.ArrayList;
44 import java.util.Comparator;
45 import java.util.List;
46 import java.util.Set;
47 
48 import javax.annotation.concurrent.NotThreadSafe;
49 
50 /**
51  * Contains issue related data.
52  *
53  * <p>Responsible for generating lists of issues and deduplication of issues.
54  */
55 @NotThreadSafe
56 final class SafetyCenterIssueRepository {
57 
58     private static final SafetySourceIssuesInfoBySeverityDescending
59             SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING =
60                     new SafetySourceIssuesInfoBySeverityDescending();
61 
62     private static final DeduplicationInfo EMPTY_DEDUP_INFO =
63             new DeduplicationInfo(emptyList(), emptyList(), emptyMap());
64 
65     private final Context mContext;
66     private final SafetySourceDataRepository mSafetySourceDataRepository;
67     private final SafetyCenterConfigReader mSafetyCenterConfigReader;
68     private final SafetyCenterIssueDismissalRepository mSafetyCenterIssueDismissalRepository;
69     private final SafetyCenterIssueDeduplicator mSafetyCenterIssueDeduplicator;
70 
71     private final SparseArray<DeduplicationInfo> mUserIdToDedupInfo = new SparseArray<>();
72 
SafetyCenterIssueRepository( Context context, SafetySourceDataRepository safetySourceDataRepository, SafetyCenterConfigReader safetyCenterConfigReader, SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository, SafetyCenterIssueDeduplicator safetyCenterIssueDeduplicator)73     SafetyCenterIssueRepository(
74             Context context,
75             SafetySourceDataRepository safetySourceDataRepository,
76             SafetyCenterConfigReader safetyCenterConfigReader,
77             SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository,
78             SafetyCenterIssueDeduplicator safetyCenterIssueDeduplicator) {
79         mContext = context;
80         mSafetySourceDataRepository = safetySourceDataRepository;
81         mSafetyCenterConfigReader = safetyCenterConfigReader;
82         mSafetyCenterIssueDismissalRepository = safetyCenterIssueDismissalRepository;
83         mSafetyCenterIssueDeduplicator = safetyCenterIssueDeduplicator;
84     }
85 
86     /**
87      * Updates the class as per the current state of issues. Should be called after any state update
88      * that can affect issues.
89      */
updateIssues(@serIdInt int userId)90     void updateIssues(@UserIdInt int userId) {
91         updateIssues(userId, UserProfileGroup.getProfileTypeOfUser(userId, mContext));
92     }
93 
updateIssues(@serIdInt int userId, @ProfileType int profileType)94     private void updateIssues(@UserIdInt int userId, @ProfileType int profileType) {
95         List<SafetySourceIssueInfo> issues =
96                 getAllStoredIssuesFromRawSourceData(userId, profileType);
97 
98         issues.sort(SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING);
99 
100         mUserIdToDedupInfo.put(userId, produceDedupInfo(issues));
101     }
102 
produceDedupInfo(List<SafetySourceIssueInfo> issues)103     private DeduplicationInfo produceDedupInfo(List<SafetySourceIssueInfo> issues) {
104         if (SdkLevel.isAtLeastU()) {
105             return mSafetyCenterIssueDeduplicator.deduplicateIssues(issues);
106         }
107         return new DeduplicationInfo(issues, emptyList(), emptyMap());
108     }
109 
110     /**
111      * Fetches a list of issues related to the given {@link UserProfileGroup}.
112      *
113      * <p>Issues in the list are sorted in descending order and deduplicated (if applicable, only on
114      * Android U+).
115      *
116      * <p>Only includes issues related to active/running {@code userId}s in the given {@link
117      * UserProfileGroup}.
118      */
getIssuesDedupedSortedDescFor(UserProfileGroup userProfileGroup)119     List<SafetySourceIssueInfo> getIssuesDedupedSortedDescFor(UserProfileGroup userProfileGroup) {
120         List<SafetySourceIssueInfo> issuesInfo = getIssuesFor(userProfileGroup);
121         issuesInfo.sort(SAFETY_SOURCE_ISSUES_INFO_BY_SEVERITY_DESCENDING);
122         return issuesInfo;
123     }
124 
125     /**
126      * Counts the total number of issues from loggable sources, in the given {@link
127      * UserProfileGroup}.
128      *
129      * <p>Only includes issues related to active/running {@code userId}s in the given {@link
130      * UserProfileGroup}.
131      */
countLoggableIssuesFor(UserProfileGroup userProfileGroup)132     int countLoggableIssuesFor(UserProfileGroup userProfileGroup) {
133         List<SafetySourceIssueInfo> relevantIssues = getIssuesFor(userProfileGroup);
134         int issueCount = 0;
135         for (int i = 0; i < relevantIssues.size(); i++) {
136             SafetySourceIssueInfo safetySourceIssueInfo = relevantIssues.get(i);
137             if (SafetySources.isLoggable(safetySourceIssueInfo.getSafetySource())) {
138                 issueCount++;
139             }
140         }
141         return issueCount;
142     }
143 
144     /** Gets a list of all issues for the given {@code userId}. */
getIssuesForUser(@serIdInt int userId)145     List<SafetySourceIssueInfo> getIssuesForUser(@UserIdInt int userId) {
146         return filterOutHiddenIssues(
147                 mUserIdToDedupInfo.get(userId, EMPTY_DEDUP_INFO).getDeduplicatedIssues());
148     }
149 
150     /**
151      * Returns a set of {@link SafetySourcesGroup} IDs that the given {@link SafetyCenterIssueKey}
152      * is mapped to, or an empty list if no such mapping is configured.
153      */
getGroupMappingFor(SafetyCenterIssueKey issueKey)154     Set<String> getGroupMappingFor(SafetyCenterIssueKey issueKey) {
155         return mUserIdToDedupInfo
156                 .get(issueKey.getUserId(), EMPTY_DEDUP_INFO)
157                 .getIssueToGroupMapping()
158                 .getOrDefault(issueKey, emptySet());
159     }
160 
161     /**
162      * Returns the list of issues for the given {@code userId} which were removed from the given
163      * list of issues in the most recent {@link SafetyCenterIssueDeduplicator#deduplicateIssues}
164      * call. These issues were removed because they were duplicates of other issues.
165      *
166      * <p>If this method is called before any calls to {@link
167      * SafetyCenterIssueDeduplicator#deduplicateIssues} then an empty list is returned.
168      */
getLatestDuplicates(@serIdInt int userId)169     List<SafetySourceIssueInfo> getLatestDuplicates(@UserIdInt int userId) {
170         return mUserIdToDedupInfo.get(userId, EMPTY_DEDUP_INFO).getFilteredOutDuplicateIssues();
171     }
172 
filterOutHiddenIssues(List<SafetySourceIssueInfo> issues)173     private List<SafetySourceIssueInfo> filterOutHiddenIssues(List<SafetySourceIssueInfo> issues) {
174         List<SafetySourceIssueInfo> result = new ArrayList<>();
175         for (int i = 0; i < issues.size(); i++) {
176             SafetySourceIssueInfo issueInfo = issues.get(i);
177             if (!mSafetyCenterIssueDismissalRepository.isIssueHidden(
178                     issueInfo.getSafetyCenterIssueKey())) {
179                 result.add(issueInfo);
180             }
181         }
182         return result;
183     }
184 
getAllStoredIssuesFromRawSourceData( @serIdInt int userId, @ProfileType int profileType)185     private List<SafetySourceIssueInfo> getAllStoredIssuesFromRawSourceData(
186             @UserIdInt int userId, @ProfileType int profileType) {
187         List<SafetySourceIssueInfo> allIssuesInfo = new ArrayList<>();
188 
189         List<SafetySourcesGroup> safetySourcesGroups =
190                 mSafetyCenterConfigReader.getSafetySourcesGroups();
191         for (int j = 0; j < safetySourcesGroups.size(); j++) {
192             addSafetySourceIssuesInfo(
193                     allIssuesInfo, safetySourcesGroups.get(j), userId, profileType);
194         }
195 
196         return allIssuesInfo;
197     }
198 
addSafetySourceIssuesInfo( List<SafetySourceIssueInfo> issuesInfo, SafetySourcesGroup safetySourcesGroup, @UserIdInt int userId, @ProfileType int profileType)199     private void addSafetySourceIssuesInfo(
200             List<SafetySourceIssueInfo> issuesInfo,
201             SafetySourcesGroup safetySourcesGroup,
202             @UserIdInt int userId,
203             @ProfileType int profileType) {
204         List<SafetySource> safetySources = safetySourcesGroup.getSafetySources();
205         for (int i = 0; i < safetySources.size(); i++) {
206             SafetySource safetySource = safetySources.get(i);
207 
208             if (!SafetySources.isExternal(safetySource)) {
209                 continue;
210             }
211             if (!SafetySources.supportsProfileType(safetySource, profileType)) {
212                 continue;
213             }
214 
215             addSafetySourceIssuesInfo(issuesInfo, safetySource, safetySourcesGroup, userId);
216         }
217     }
218 
addSafetySourceIssuesInfo( List<SafetySourceIssueInfo> issuesInfo, SafetySource safetySource, SafetySourcesGroup safetySourcesGroup, @UserIdInt int userId)219     private void addSafetySourceIssuesInfo(
220             List<SafetySourceIssueInfo> issuesInfo,
221             SafetySource safetySource,
222             SafetySourcesGroup safetySourcesGroup,
223             @UserIdInt int userId) {
224         SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId);
225         SafetySourceData safetySourceData = mSafetySourceDataRepository.getSafetySourceData(key);
226 
227         if (safetySourceData == null) {
228             return;
229         }
230 
231         List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues();
232         for (int i = 0; i < safetySourceIssues.size(); i++) {
233             SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
234 
235             SafetySourceIssueInfo safetySourceIssueInfo =
236                     new SafetySourceIssueInfo(
237                             safetySourceIssue, safetySource, safetySourcesGroup, userId);
238             issuesInfo.add(safetySourceIssueInfo);
239         }
240     }
241 
242     /**
243      * Only includes issues related to active/running {@code userId}s in the given {@link
244      * UserProfileGroup}.
245      */
getIssuesFor(UserProfileGroup userProfileGroup)246     private List<SafetySourceIssueInfo> getIssuesFor(UserProfileGroup userProfileGroup) {
247         List<SafetySourceIssueInfo> issues = new ArrayList<>();
248 
249         int[] allRunningProfileUserIds = userProfileGroup.getAllRunningProfilesUserIds();
250         for (int i = 0; i < allRunningProfileUserIds.length; i++) {
251             issues.addAll(getIssuesForUser(allRunningProfileUserIds[i]));
252         }
253 
254         return issues;
255     }
256 
257     /** A comparator to order {@link SafetySourceIssueInfo} by severity level descending. */
258     private static final class SafetySourceIssuesInfoBySeverityDescending
259             implements Comparator<SafetySourceIssueInfo> {
260 
SafetySourceIssuesInfoBySeverityDescending()261         private SafetySourceIssuesInfoBySeverityDescending() {}
262 
263         @Override
compare(SafetySourceIssueInfo left, SafetySourceIssueInfo right)264         public int compare(SafetySourceIssueInfo left, SafetySourceIssueInfo right) {
265             return Integer.compare(
266                     right.getSafetySourceIssue().getSeverityLevel(),
267                     left.getSafetySourceIssue().getSeverityLevel());
268         }
269     }
270 
271     /** Dumps state for debugging purposes. */
dump(PrintWriter fout)272     void dump(PrintWriter fout) {
273         fout.println("ISSUE REPOSITORY");
274         for (int i = 0; i < mUserIdToDedupInfo.size(); i++) {
275             fout.println();
276             fout.println("\tUSER ID=" + mUserIdToDedupInfo.keyAt(i));
277             fout.println("\tDEDUPLICATION INFO=" + mUserIdToDedupInfo.valueAt(i));
278         }
279         fout.println();
280     }
281 
282     /** Clears all the data from the repository. */
clear()283     void clear() {
284         mUserIdToDedupInfo.clear();
285     }
286 
287     /** Clears all data related to the given {@code userId}. */
clearForUser(@serIdInt int userId)288     void clearForUser(@UserIdInt int userId) {
289         mUserIdToDedupInfo.delete(userId);
290     }
291 }
292