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 android.annotation.ElapsedRealtimeLong;
20 import android.content.Context;
21 import android.safetycenter.SafetyCenterManager;
22 import android.safetycenter.SafetyEvent;
23 import android.safetycenter.SafetySourceData;
24 import android.safetycenter.SafetySourceIssue;
25 import android.safetycenter.SafetySourceStatus;
26 
27 import androidx.annotation.Nullable;
28 
29 import com.android.safetycenter.SafetySourceIssueInfo;
30 import com.android.safetycenter.SafetySourceKey;
31 import com.android.safetycenter.UserProfileGroup;
32 import com.android.safetycenter.UserProfileGroup.ProfileType;
33 import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
34 import com.android.safetycenter.logging.SafetyCenterStatsdLogger;
35 
36 import java.util.Collections;
37 import java.util.List;
38 
39 import javax.annotation.concurrent.NotThreadSafe;
40 
41 /**
42  * Collates information from various data-related classes and uses that information to log {@code
43  * SafetySourceStateCollected} atoms.
44  */
45 @NotThreadSafe
46 final class SafetySourceStateCollectedLogger {
47 
48     private final Context mContext;
49     private final SafetySourceDataRepository mSourceDataRepository;
50     private final SafetyCenterIssueDismissalRepository mIssueDismissalRepository;
51     private final SafetyCenterIssueRepository mIssueRepository;
52 
SafetySourceStateCollectedLogger( Context context, SafetySourceDataRepository sourceDataRepository, SafetyCenterIssueDismissalRepository issueDismissalRepository, SafetyCenterIssueRepository issueRepository)53     SafetySourceStateCollectedLogger(
54             Context context,
55             SafetySourceDataRepository sourceDataRepository,
56             SafetyCenterIssueDismissalRepository issueDismissalRepository,
57             SafetyCenterIssueRepository issueRepository) {
58         mContext = context;
59         mSourceDataRepository = sourceDataRepository;
60         mIssueDismissalRepository = issueDismissalRepository;
61         mIssueRepository = issueRepository;
62     }
63 
64     /**
65      * Writes a SafetySourceStateCollected atom for the given source in response to a stats pull.
66      */
writeAutomaticAtom(SafetySourceKey sourceKey, @ProfileType int profileType)67     void writeAutomaticAtom(SafetySourceKey sourceKey, @ProfileType int profileType) {
68         logSafetySourceStateCollected(
69                 sourceKey,
70                 mSourceDataRepository.getSafetySourceData(sourceKey),
71                 /* refreshReason= */ null,
72                 /* sourceDataDiffers= */ false,
73                 profileType,
74                 /* safetyEvent= */ null,
75                 mSourceDataRepository.getSafetySourceLastUpdated(sourceKey));
76     }
77 
78     /**
79      * Writes a SafetySourceStateCollected atom for the given source in response to that source
80      * updating its own state.
81      */
writeSourceUpdatedAtom( SafetySourceKey key, @Nullable SafetySourceData safetySourceData, @Nullable @SafetyCenterManager.RefreshReason Integer refreshReason, boolean sourceDataDiffers, int userId, SafetyEvent safetyEvent)82     void writeSourceUpdatedAtom(
83             SafetySourceKey key,
84             @Nullable SafetySourceData safetySourceData,
85             @Nullable @SafetyCenterManager.RefreshReason Integer refreshReason,
86             boolean sourceDataDiffers,
87             int userId,
88             SafetyEvent safetyEvent) {
89         logSafetySourceStateCollected(
90                 key,
91                 safetySourceData,
92                 refreshReason,
93                 sourceDataDiffers,
94                 UserProfileGroup.getProfileTypeOfUser(userId, mContext),
95                 safetyEvent,
96                 /* lastUpdatedElapsedTimeMillis= */ null);
97     }
98 
logSafetySourceStateCollected( SafetySourceKey sourceKey, @Nullable SafetySourceData sourceData, @Nullable @SafetyCenterManager.RefreshReason Integer refreshReason, boolean sourceDataDiffers, @ProfileType int profileType, @Nullable SafetyEvent safetyEvent, @Nullable @ElapsedRealtimeLong Long lastUpdatedElapsedTimeMillis)99     private void logSafetySourceStateCollected(
100             SafetySourceKey sourceKey,
101             @Nullable SafetySourceData sourceData,
102             @Nullable @SafetyCenterManager.RefreshReason Integer refreshReason,
103             boolean sourceDataDiffers,
104             @ProfileType int profileType,
105             @Nullable SafetyEvent safetyEvent,
106             @Nullable @ElapsedRealtimeLong Long lastUpdatedElapsedTimeMillis) {
107         SafetySourceStatus sourceStatus = sourceData == null ? null : sourceData.getStatus();
108         List<SafetySourceIssue> sourceIssues =
109                 sourceData == null ? Collections.emptyList() : sourceData.getIssues();
110 
111         int maxSeverityLevel = Integer.MIN_VALUE;
112         if (sourceStatus != null) {
113             maxSeverityLevel = sourceStatus.getSeverityLevel();
114         } else if (sourceData != null) {
115             // In this case we know we have an issue-only source because of the checks carried out
116             // in the validateRequest function.
117             maxSeverityLevel = SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED;
118         }
119 
120         long openIssuesCount = 0;
121         long dismissedIssuesCount = 0;
122         for (int i = 0; i < sourceIssues.size(); i++) {
123             SafetySourceIssue issue = sourceIssues.get(i);
124             if (isIssueDismissed(issue, sourceKey)) {
125                 dismissedIssuesCount++;
126             } else {
127                 openIssuesCount++;
128                 maxSeverityLevel = Math.max(maxSeverityLevel, issue.getSeverityLevel());
129             }
130         }
131 
132         Integer severityLevel = maxSeverityLevel > Integer.MIN_VALUE ? maxSeverityLevel : null;
133         SafetyCenterStatsdLogger.writeSafetySourceStateCollected(
134                 sourceKey.getSourceId(),
135                 profileType,
136                 severityLevel,
137                 openIssuesCount,
138                 dismissedIssuesCount,
139                 getDuplicateCount(sourceKey),
140                 mSourceDataRepository.getSourceState(sourceKey),
141                 safetyEvent,
142                 refreshReason,
143                 sourceDataDiffers,
144                 lastUpdatedElapsedTimeMillis);
145     }
146 
isIssueDismissed(SafetySourceIssue issue, SafetySourceKey sourceKey)147     private boolean isIssueDismissed(SafetySourceIssue issue, SafetySourceKey sourceKey) {
148         SafetyCenterIssueKey issueKey =
149                 SafetyCenterIssueKey.newBuilder()
150                         .setSafetySourceId(sourceKey.getSourceId())
151                         .setSafetySourceIssueId(issue.getId())
152                         .setUserId(sourceKey.getUserId())
153                         .build();
154         return mIssueDismissalRepository.isIssueDismissed(issueKey, issue.getSeverityLevel());
155     }
156 
getDuplicateCount(SafetySourceKey sourceKey)157     private long getDuplicateCount(SafetySourceKey sourceKey) {
158         long count = 0;
159         List<SafetySourceIssueInfo> duplicates =
160                 mIssueRepository.getLatestDuplicates(sourceKey.getUserId());
161         for (int i = 0; i < duplicates.size(); i++) {
162             if (duplicates.get(i).getSafetySource().getId().equals(sourceKey.getSourceId())) {
163                 count++;
164             }
165         }
166         return count;
167     }
168 }
169