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.internaldata.SafetyCenterIds.toUserFriendlyString;
20 
21 import android.annotation.UserIdInt;
22 import android.content.Context;
23 import android.os.SystemClock;
24 import android.safetycenter.SafetySourceIssue;
25 import android.util.ArrayMap;
26 import android.util.ArraySet;
27 import android.util.Log;
28 
29 import androidx.annotation.Nullable;
30 
31 import com.android.safetycenter.SafetySourceIssues;
32 import com.android.safetycenter.UserProfileGroup;
33 import com.android.safetycenter.internaldata.SafetyCenterIssueActionId;
34 import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
35 import com.android.safetycenter.logging.SafetyCenterStatsdLogger;
36 
37 import java.io.PrintWriter;
38 import java.time.Duration;
39 
40 import javax.annotation.concurrent.NotThreadSafe;
41 
42 /** Maintains data about in-flight issue actions. */
43 @NotThreadSafe
44 final class SafetyCenterInFlightIssueActionRepository {
45 
46     private static final String TAG = "SafetyCenterInFlight";
47 
48     private final ArrayMap<SafetyCenterIssueActionId, Long> mSafetyCenterIssueActionsInFlight =
49             new ArrayMap<>();
50 
51     private final Context mContext;
52 
53     /** Constructs a new instance of {@link SafetyCenterInFlightIssueActionRepository}. */
SafetyCenterInFlightIssueActionRepository(Context context)54     SafetyCenterInFlightIssueActionRepository(Context context) {
55         mContext = context;
56     }
57 
58     /** Marks the given {@link SafetyCenterIssueActionId} as in-flight. */
markSafetyCenterIssueActionInFlight(SafetyCenterIssueActionId safetyCenterIssueActionId)59     void markSafetyCenterIssueActionInFlight(SafetyCenterIssueActionId safetyCenterIssueActionId) {
60         mSafetyCenterIssueActionsInFlight.put(
61                 safetyCenterIssueActionId, SystemClock.elapsedRealtime());
62     }
63 
64     /**
65      * Unmarks the given {@link SafetyCenterIssueActionId} as in-flight and returns {@code true} if
66      * the given action was valid and unmarked successfully.
67      *
68      * <p>Also logs an event to statsd with the given {@code result} value.
69      */
unmarkSafetyCenterIssueActionInFlight( SafetyCenterIssueActionId safetyCenterIssueActionId, @Nullable SafetySourceIssue safetySourceIssue, @SafetyCenterStatsdLogger.SystemEventResult int result)70     boolean unmarkSafetyCenterIssueActionInFlight(
71             SafetyCenterIssueActionId safetyCenterIssueActionId,
72             @Nullable SafetySourceIssue safetySourceIssue,
73             @SafetyCenterStatsdLogger.SystemEventResult int result) {
74         Long startElapsedMillis =
75                 mSafetyCenterIssueActionsInFlight.remove(safetyCenterIssueActionId);
76         if (startElapsedMillis == null) {
77             Log.w(
78                     TAG,
79                     "Attempt to unmark unknown in-flight action: "
80                             + toUserFriendlyString(safetyCenterIssueActionId));
81             return false;
82         }
83 
84         SafetyCenterIssueKey issueKey = safetyCenterIssueActionId.getSafetyCenterIssueKey();
85         String issueTypeId = safetySourceIssue == null ? null : safetySourceIssue.getIssueTypeId();
86         Duration duration = Duration.ofMillis(SystemClock.elapsedRealtime() - startElapsedMillis);
87 
88         SafetyCenterStatsdLogger.writeInlineActionSystemEvent(
89                 issueKey.getSafetySourceId(),
90                 UserProfileGroup.getProfileTypeOfUser(issueKey.getUserId(), mContext),
91                 issueTypeId,
92                 duration,
93                 result);
94 
95         if (safetySourceIssue == null
96                 || getSafetySourceIssueAction(safetyCenterIssueActionId, safetySourceIssue)
97                         == null) {
98             Log.w(
99                     TAG,
100                     "Attempt to unmark in-flight action for a non-existent issue or action: "
101                             + toUserFriendlyString(safetyCenterIssueActionId));
102             return false;
103         }
104 
105         return true;
106     }
107 
108     /** Returns {@code true} if the given issue action is in flight. */
actionIsInFlight(SafetyCenterIssueActionId safetyCenterIssueActionId)109     boolean actionIsInFlight(SafetyCenterIssueActionId safetyCenterIssueActionId) {
110         return mSafetyCenterIssueActionsInFlight.containsKey(safetyCenterIssueActionId);
111     }
112 
113     /** Returns a list of IDs of in-flight actions for the given source and user */
getInFlightActions(String sourceId, @UserIdInt int userId)114     ArraySet<SafetyCenterIssueActionId> getInFlightActions(String sourceId, @UserIdInt int userId) {
115         ArraySet<SafetyCenterIssueActionId> result = new ArraySet<>();
116         for (int i = 0; i < mSafetyCenterIssueActionsInFlight.size(); i++) {
117             SafetyCenterIssueActionId actionId = mSafetyCenterIssueActionsInFlight.keyAt(i);
118             SafetyCenterIssueKey issueKey = actionId.getSafetyCenterIssueKey();
119             if (sourceId.equals(issueKey.getSafetySourceId()) && issueKey.getUserId() == userId) {
120                 result.add(actionId);
121             }
122         }
123         return result;
124     }
125 
126     /**
127      * Returns {@link SafetySourceIssue.Action} identified by the given {@link
128      * SafetyCenterIssueActionId} and {@link SafetySourceIssue}.
129      */
130     @Nullable
getSafetySourceIssueAction( SafetyCenterIssueActionId safetyCenterIssueActionId, SafetySourceIssue safetySourceIssue)131     SafetySourceIssue.Action getSafetySourceIssueAction(
132             SafetyCenterIssueActionId safetyCenterIssueActionId,
133             SafetySourceIssue safetySourceIssue) {
134         if (actionIsInFlight(safetyCenterIssueActionId)) {
135             return null;
136         }
137 
138         return SafetySourceIssues.findAction(
139                 safetySourceIssue, safetyCenterIssueActionId.getSafetySourceIssueActionId());
140     }
141 
142     /** Dumps in-flight action data for debugging purposes. */
dump(PrintWriter fout)143     void dump(PrintWriter fout) {
144         int actionInFlightCount = mSafetyCenterIssueActionsInFlight.size();
145         fout.println("ACTIONS IN FLIGHT (" + actionInFlightCount + ")");
146         for (int i = 0; i < actionInFlightCount; i++) {
147             String printableId = toUserFriendlyString(mSafetyCenterIssueActionsInFlight.keyAt(i));
148             long startElapsedMillis = mSafetyCenterIssueActionsInFlight.valueAt(i);
149             long durationMillis = SystemClock.elapsedRealtime() - startElapsedMillis;
150             fout.println("\t[" + i + "] " + printableId + "(duration=" + durationMillis + "ms)");
151         }
152         fout.println();
153     }
154 
155     /** Clears all in-flight action data. */
clear()156     void clear() {
157         mSafetyCenterIssueActionsInFlight.clear();
158     }
159 
160     /** Clears in-flight action data for given {@code userId}. */
clearForUser(@serIdInt int userId)161     void clearForUser(@UserIdInt int userId) {
162         // Loop in reverse index order to be able to remove entries while iterating.
163         for (int i = mSafetyCenterIssueActionsInFlight.size() - 1; i >= 0; i--) {
164             SafetyCenterIssueActionId issueActionId = mSafetyCenterIssueActionsInFlight.keyAt(i);
165             if (issueActionId.getSafetyCenterIssueKey().getUserId() == userId) {
166                 mSafetyCenterIssueActionsInFlight.removeAt(i);
167             }
168         }
169     }
170 }
171