1 /*
2  * Copyright (C) 2021 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 android.safetycenter;
18 
19 import static android.os.Build.VERSION_CODES.TIRAMISU;
20 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
21 
22 import static com.android.internal.util.Preconditions.checkArgument;
23 
24 import static java.util.Collections.unmodifiableList;
25 import static java.util.Objects.requireNonNull;
26 
27 import android.annotation.IntDef;
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.annotation.SystemApi;
31 import android.os.Bundle;
32 import android.os.Parcel;
33 import android.os.Parcelable;
34 
35 import androidx.annotation.RequiresApi;
36 
37 import com.android.modules.utils.build.SdkLevel;
38 
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.util.ArrayList;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Objects;
45 import java.util.Set;
46 
47 /**
48  * Data class used by safety sources to propagate safety information such as their safety status and
49  * safety issues.
50  *
51  * @hide
52  */
53 @SystemApi
54 @RequiresApi(TIRAMISU)
55 public final class SafetySourceData implements Parcelable {
56 
57     /**
58      * Indicates that no opinion is currently associated with the information provided.
59      *
60      * <p>This severity level will be reflected in the UI of a {@link SafetySourceStatus} through a
61      * grey icon.
62      *
63      * <p>For a {@link SafetySourceStatus}, this severity level indicates that the safety source
64      * currently does not have sufficient information on the severity level of the {@link
65      * SafetySourceStatus}.
66      *
67      * <p>This severity level cannot be used to indicate the severity level of a {@link
68      * SafetySourceIssue}.
69      */
70     public static final int SEVERITY_LEVEL_UNSPECIFIED = 100;
71 
72     /**
73      * Indicates the presence of an informational message or the absence of any safety issues.
74      *
75      * <p>This severity level will be reflected in the UI of either a {@link SafetySourceStatus} or
76      * a {@link SafetySourceIssue} through a green icon.
77      *
78      * <p>For a {@link SafetySourceStatus}, this severity level indicates either the absence of any
79      * {@link SafetySourceIssue}s or the presence of only {@link SafetySourceIssue}s with the same
80      * severity level.
81      *
82      * <p>For a {@link SafetySourceIssue}, this severity level indicates that the {@link
83      * SafetySourceIssue} represents an informational message relating to the safety source. {@link
84      * SafetySourceIssue}s of this severity level will be dismissible by the user from the UI, and
85      * will not trigger a confirmation dialog upon a user attempting to dismiss the warning.
86      */
87     public static final int SEVERITY_LEVEL_INFORMATION = 200;
88 
89     /**
90      * Indicates the presence of a medium-severity safety issue which the user is encouraged to act
91      * on.
92      *
93      * <p>This severity level will be reflected in the UI of either a {@link SafetySourceStatus} or
94      * a {@link SafetySourceIssue} through a yellow icon.
95      *
96      * <p>For a {@link SafetySourceStatus}, this severity level indicates the presence of at least
97      * one medium-severity {@link SafetySourceIssue} relating to the safety source which the user is
98      * encouraged to act on, and no {@link SafetySourceIssue}s with higher severity level.
99      *
100      * <p>For a {@link SafetySourceIssue}, this severity level indicates that the {@link
101      * SafetySourceIssue} represents a medium-severity safety issue relating to the safety source
102      * which the user is encouraged to act on. {@link SafetySourceIssue}s of this severity level
103      * will be dismissible by the user from the UI, and will trigger a confirmation dialog upon a
104      * user attempting to dismiss the warning.
105      */
106     public static final int SEVERITY_LEVEL_RECOMMENDATION = 300;
107 
108     /**
109      * Indicates the presence of a critical or urgent safety issue that should be addressed by the
110      * user.
111      *
112      * <p>This severity level will be reflected in the UI of either a {@link SafetySourceStatus} or
113      * a {@link SafetySourceIssue} through a red icon.
114      *
115      * <p>For a {@link SafetySourceStatus}, this severity level indicates the presence of at least
116      * one critical or urgent {@link SafetySourceIssue} relating to the safety source that should be
117      * addressed by the user.
118      *
119      * <p>For a {@link SafetySourceIssue}, this severity level indicates that the {@link
120      * SafetySourceIssue} represents a critical or urgent safety issue relating to the safety source
121      * that should be addressed by the user. {@link SafetySourceIssue}s of this severity level will
122      * be dismissible by the user from the UI, and will trigger a confirmation dialog upon a user
123      * attempting to dismiss the warning.
124      */
125     public static final int SEVERITY_LEVEL_CRITICAL_WARNING = 400;
126 
127     /**
128      * All possible severity levels for a {@link SafetySourceStatus} or a {@link SafetySourceIssue}.
129      *
130      * <p>The numerical values of the levels are not used directly, rather they are used to build a
131      * continuum of levels which support relative comparison. The higher the severity level the
132      * higher the threat to the user.
133      *
134      * <p>For a {@link SafetySourceStatus}, the severity level is meant to convey the aggregated
135      * severity of the safety source, and it contributes to the overall severity level in the Safety
136      * Center. If the {@link SafetySourceData} contains {@link SafetySourceIssue}s, the severity
137      * level of the s{@link SafetySourceStatus} must match the highest severity level among the
138      * {@link SafetySourceIssue}s.
139      *
140      * <p>For a {@link SafetySourceIssue}, not all severity levels can be used. The severity level
141      * also determines how a {@link SafetySourceIssue}s is "dismissible" by the user, i.e. how the
142      * user can choose to ignore the issue and remove it from view in the Safety Center.
143      *
144      * @hide
145      */
146     @IntDef(
147             prefix = {"SEVERITY_LEVEL_"},
148             value = {
149                 SEVERITY_LEVEL_UNSPECIFIED,
150                 SEVERITY_LEVEL_INFORMATION,
151                 SEVERITY_LEVEL_RECOMMENDATION,
152                 SEVERITY_LEVEL_CRITICAL_WARNING
153             })
154     @Retention(RetentionPolicy.SOURCE)
155     public @interface SeverityLevel {}
156 
157     @NonNull
158     public static final Creator<SafetySourceData> CREATOR =
159             new Creator<SafetySourceData>() {
160                 @Override
161                 public SafetySourceData createFromParcel(Parcel in) {
162                     SafetySourceStatus status = in.readTypedObject(SafetySourceStatus.CREATOR);
163                     List<SafetySourceIssue> issues =
164                             requireNonNull(in.createTypedArrayList(SafetySourceIssue.CREATOR));
165                     Builder builder = new Builder().setStatus(status);
166                     for (int i = 0; i < issues.size(); i++) {
167                         builder.addIssue(issues.get(i));
168                     }
169                     if (SdkLevel.isAtLeastU()) {
170                         Bundle extras = in.readBundle(getClass().getClassLoader());
171                         if (extras != null) {
172                             builder.setExtras(extras);
173                         }
174                     }
175                     return builder.build();
176                 }
177 
178                 @Override
179                 public SafetySourceData[] newArray(int size) {
180                     return new SafetySourceData[size];
181                 }
182             };
183 
184     @Nullable private final SafetySourceStatus mStatus;
185     @NonNull private final List<SafetySourceIssue> mIssues;
186     @NonNull private final Bundle mExtras;
187 
SafetySourceData( @ullable SafetySourceStatus status, @NonNull List<SafetySourceIssue> issues, @NonNull Bundle extras)188     private SafetySourceData(
189             @Nullable SafetySourceStatus status,
190             @NonNull List<SafetySourceIssue> issues,
191             @NonNull Bundle extras) {
192         this.mStatus = status;
193         this.mIssues = issues;
194         this.mExtras = extras;
195     }
196 
197     /** Returns the data for the {@link SafetySourceStatus} to be shown in UI. */
198     @Nullable
getStatus()199     public SafetySourceStatus getStatus() {
200         return mStatus;
201     }
202 
203     /** Returns the data for the list of {@link SafetySourceIssue}s to be shown in UI. */
204     @NonNull
getIssues()205     public List<SafetySourceIssue> getIssues() {
206         return mIssues;
207     }
208 
209     /**
210      * Returns a {@link Bundle} containing additional information, {@link Bundle#EMPTY} by default.
211      *
212      * <p>Note: internal state of this {@link Bundle} is not used for {@link Object#equals} and
213      * {@link Object#hashCode} implementation of {@link SafetySourceData}.
214      */
215     @NonNull
216     @RequiresApi(UPSIDE_DOWN_CAKE)
getExtras()217     public Bundle getExtras() {
218         if (!SdkLevel.isAtLeastU()) {
219             throw new UnsupportedOperationException(
220                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
221         }
222         return mExtras;
223     }
224 
225     @Override
describeContents()226     public int describeContents() {
227         return 0;
228     }
229 
230     @Override
writeToParcel(@onNull Parcel dest, int flags)231     public void writeToParcel(@NonNull Parcel dest, int flags) {
232         dest.writeTypedObject(mStatus, flags);
233         dest.writeTypedList(mIssues);
234         if (SdkLevel.isAtLeastU()) {
235             dest.writeBundle(mExtras);
236         }
237     }
238 
239     @Override
equals(Object o)240     public boolean equals(Object o) {
241         if (this == o) return true;
242         if (!(o instanceof SafetySourceData)) return false;
243         SafetySourceData that = (SafetySourceData) o;
244         return Objects.equals(mStatus, that.mStatus) && mIssues.equals(that.mIssues);
245     }
246 
247     @Override
hashCode()248     public int hashCode() {
249         return Objects.hash(mStatus, mIssues);
250     }
251 
252     @Override
toString()253     public String toString() {
254         return "SafetySourceData{"
255                 + "mStatus="
256                 + mStatus
257                 + ", mIssues="
258                 + mIssues
259                 + (!mExtras.isEmpty() ? ", (has extras)" : "")
260                 + '}';
261     }
262 
263     /** Builder class for {@link SafetySourceData}. */
264     public static final class Builder {
265 
266         @NonNull private final List<SafetySourceIssue> mIssues = new ArrayList<>();
267 
268         @Nullable private SafetySourceStatus mStatus;
269         @NonNull private Bundle mExtras = Bundle.EMPTY;
270 
271         /** Creates a {@link Builder} for a {@link SafetySourceData}. */
Builder()272         public Builder() {}
273 
274         /** Creates a {@link Builder} with the values from the given {@link SafetySourceData}. */
275         @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull SafetySourceData safetySourceData)276         public Builder(@NonNull SafetySourceData safetySourceData) {
277             if (!SdkLevel.isAtLeastU()) {
278                 throw new UnsupportedOperationException(
279                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
280             }
281             requireNonNull(safetySourceData);
282             mIssues.addAll(safetySourceData.mIssues);
283             mStatus = safetySourceData.mStatus;
284             mExtras = safetySourceData.mExtras.deepCopy();
285         }
286 
287         /** Sets data for the {@link SafetySourceStatus} to be shown in UI. */
288         @NonNull
setStatus(@ullable SafetySourceStatus status)289         public Builder setStatus(@Nullable SafetySourceStatus status) {
290             mStatus = status;
291             return this;
292         }
293 
294         /** Adds data for a {@link SafetySourceIssue} to be shown in UI. */
295         @NonNull
addIssue(@onNull SafetySourceIssue safetySourceIssue)296         public Builder addIssue(@NonNull SafetySourceIssue safetySourceIssue) {
297             mIssues.add(requireNonNull(safetySourceIssue));
298             return this;
299         }
300 
301         /**
302          * Sets additional information for the {@link SafetySourceData}.
303          *
304          * <p>If not set, the default value is {@link Bundle#EMPTY}.
305          */
306         @NonNull
307         @RequiresApi(UPSIDE_DOWN_CAKE)
setExtras(@onNull Bundle extras)308         public Builder setExtras(@NonNull Bundle extras) {
309             if (!SdkLevel.isAtLeastU()) {
310                 throw new UnsupportedOperationException(
311                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
312             }
313             mExtras = requireNonNull(extras);
314             return this;
315         }
316 
317         /**
318          * Resets additional information for the {@link SafetySourceData} to the default value of
319          * {@link Bundle#EMPTY}.
320          */
321         @NonNull
322         @RequiresApi(UPSIDE_DOWN_CAKE)
clearExtras()323         public Builder clearExtras() {
324             if (!SdkLevel.isAtLeastU()) {
325                 throw new UnsupportedOperationException(
326                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
327             }
328             mExtras = Bundle.EMPTY;
329             return this;
330         }
331 
332         /**
333          * Clears data for all the {@link SafetySourceIssue}s that were added to this {@link
334          * Builder}.
335          */
336         @NonNull
clearIssues()337         public Builder clearIssues() {
338             mIssues.clear();
339             return this;
340         }
341 
342         /** Creates the {@link SafetySourceData} defined by this {@link Builder}. */
343         @NonNull
build()344         public SafetySourceData build() {
345             List<SafetySourceIssue> issues = unmodifiableList(new ArrayList<>(mIssues));
346             int issuesMaxSeverityLevel = getIssuesMaxSeverityLevelEnforcingUniqueIds(issues);
347             if (mStatus == null) {
348                 return new SafetySourceData(null, issues, mExtras);
349             }
350             int statusSeverityLevel = mStatus.getSeverityLevel();
351             boolean requiresAttention = issuesMaxSeverityLevel > SEVERITY_LEVEL_INFORMATION;
352             if (requiresAttention) {
353                 checkArgument(
354                         statusSeverityLevel >= issuesMaxSeverityLevel,
355                         "Safety source data cannot have issues that are more severe than its"
356                                 + " status");
357             }
358 
359             return new SafetySourceData(mStatus, issues, mExtras);
360         }
361 
getIssuesMaxSeverityLevelEnforcingUniqueIds( @onNull List<SafetySourceIssue> issues)362         private static int getIssuesMaxSeverityLevelEnforcingUniqueIds(
363                 @NonNull List<SafetySourceIssue> issues) {
364             int max = Integer.MIN_VALUE;
365             Set<String> issueIds = new HashSet<>();
366             for (int i = 0; i < issues.size(); i++) {
367                 SafetySourceIssue safetySourceIssue = issues.get(i);
368 
369                 String issueId = safetySourceIssue.getId();
370                 checkArgument(
371                         !issueIds.contains(issueId),
372                         "Safety source data cannot have duplicate issue ids");
373                 max = Math.max(max, safetySourceIssue.getSeverityLevel());
374                 issueIds.add(issueId);
375             }
376             return max;
377         }
378     }
379 }
380