1 /*
2  * Copyright (C) 2022 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 java.util.Collections.unmodifiableList;
23 import static java.util.Objects.requireNonNull;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.SystemApi;
28 import android.os.Bundle;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 
32 import androidx.annotation.RequiresApi;
33 
34 import com.android.modules.utils.build.SdkLevel;
35 
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Objects;
39 import java.util.Set;
40 
41 /**
42  * A representation of the safety state of the device.
43  *
44  * @hide
45  */
46 @SystemApi
47 @RequiresApi(TIRAMISU)
48 public final class SafetyCenterData implements Parcelable {
49 
50     /**
51      * A key used in {@link #getExtras()} to map {@link SafetyCenterIssue} ids to their associated
52      * {@link SafetyCenterEntryGroup} ids.
53      */
54     private static final String ISSUES_TO_GROUPS_BUNDLE_KEY = "IssuesToGroups";
55 
56     /**
57      * A key used in {@link #getExtras()} to map {@link SafetyCenterStaticEntry} to their associated
58      * ids.
59      *
60      * <p>{@link SafetyCenterStaticEntry} are keyed by {@code
61      * SafetyCenterIds.toBundleKey(safetyCenterStaticEntry)}.
62      */
63     private static final String STATIC_ENTRIES_TO_IDS_BUNDLE_KEY = "StaticEntriesToIds";
64 
65     @NonNull
66     public static final Creator<SafetyCenterData> CREATOR =
67             new Creator<SafetyCenterData>() {
68                 @Override
69                 public SafetyCenterData createFromParcel(Parcel in) {
70                     SafetyCenterStatus status = in.readTypedObject(SafetyCenterStatus.CREATOR);
71                     List<SafetyCenterIssue> issues =
72                             in.createTypedArrayList(SafetyCenterIssue.CREATOR);
73                     List<SafetyCenterEntryOrGroup> entryOrGroups =
74                             in.createTypedArrayList(SafetyCenterEntryOrGroup.CREATOR);
75                     List<SafetyCenterStaticEntryGroup> staticEntryGroups =
76                             in.createTypedArrayList(SafetyCenterStaticEntryGroup.CREATOR);
77 
78                     if (SdkLevel.isAtLeastU()) {
79                         List<SafetyCenterIssue> dismissedIssues =
80                                 in.createTypedArrayList(SafetyCenterIssue.CREATOR);
81                         Bundle extras = in.readBundle(getClass().getClassLoader());
82                         SafetyCenterData.Builder builder = new SafetyCenterData.Builder(status);
83                         for (int i = 0; i < issues.size(); i++) {
84                             builder.addIssue(issues.get(i));
85                         }
86                         for (int i = 0; i < entryOrGroups.size(); i++) {
87                             builder.addEntryOrGroup(entryOrGroups.get(i));
88                         }
89                         for (int i = 0; i < staticEntryGroups.size(); i++) {
90                             builder.addStaticEntryGroup(staticEntryGroups.get(i));
91                         }
92                         for (int i = 0; i < dismissedIssues.size(); i++) {
93                             builder.addDismissedIssue(dismissedIssues.get(i));
94                         }
95                         if (extras != null) {
96                             builder.setExtras(extras);
97                         }
98                         return builder.build();
99                     } else {
100                         return new SafetyCenterData(
101                                 status, issues, entryOrGroups, staticEntryGroups);
102                     }
103                 }
104 
105                 @Override
106                 public SafetyCenterData[] newArray(int size) {
107                     return new SafetyCenterData[size];
108                 }
109             };
110 
111     @NonNull private final SafetyCenterStatus mStatus;
112     @NonNull private final List<SafetyCenterIssue> mIssues;
113     @NonNull private final List<SafetyCenterEntryOrGroup> mEntriesOrGroups;
114     @NonNull private final List<SafetyCenterStaticEntryGroup> mStaticEntryGroups;
115     @NonNull private final List<SafetyCenterIssue> mDismissedIssues;
116     @NonNull private final Bundle mExtras;
117 
118     /** Creates a {@link SafetyCenterData}. */
SafetyCenterData( @onNull SafetyCenterStatus status, @NonNull List<SafetyCenterIssue> issues, @NonNull List<SafetyCenterEntryOrGroup> entriesOrGroups, @NonNull List<SafetyCenterStaticEntryGroup> staticEntryGroups)119     public SafetyCenterData(
120             @NonNull SafetyCenterStatus status,
121             @NonNull List<SafetyCenterIssue> issues,
122             @NonNull List<SafetyCenterEntryOrGroup> entriesOrGroups,
123             @NonNull List<SafetyCenterStaticEntryGroup> staticEntryGroups) {
124         mStatus = requireNonNull(status);
125         mIssues = unmodifiableList(new ArrayList<>(requireNonNull(issues)));
126         mEntriesOrGroups = unmodifiableList(new ArrayList<>(requireNonNull(entriesOrGroups)));
127         mStaticEntryGroups = unmodifiableList(new ArrayList<>(requireNonNull(staticEntryGroups)));
128         mDismissedIssues = unmodifiableList(new ArrayList<>());
129         mExtras = Bundle.EMPTY;
130     }
131 
SafetyCenterData( @onNull SafetyCenterStatus status, @NonNull List<SafetyCenterIssue> issues, @NonNull List<SafetyCenterEntryOrGroup> entriesOrGroups, @NonNull List<SafetyCenterStaticEntryGroup> staticEntryGroups, @NonNull List<SafetyCenterIssue> dismissedIssues, @NonNull Bundle extras)132     private SafetyCenterData(
133             @NonNull SafetyCenterStatus status,
134             @NonNull List<SafetyCenterIssue> issues,
135             @NonNull List<SafetyCenterEntryOrGroup> entriesOrGroups,
136             @NonNull List<SafetyCenterStaticEntryGroup> staticEntryGroups,
137             @NonNull List<SafetyCenterIssue> dismissedIssues,
138             @NonNull Bundle extras) {
139         mStatus = status;
140         mIssues = issues;
141         mEntriesOrGroups = entriesOrGroups;
142         mStaticEntryGroups = staticEntryGroups;
143         mDismissedIssues = dismissedIssues;
144         mExtras = extras;
145     }
146 
147     /** Returns the overall {@link SafetyCenterStatus} of the Safety Center. */
148     @NonNull
getStatus()149     public SafetyCenterStatus getStatus() {
150         return mStatus;
151     }
152 
153     /** Returns the list of active {@link SafetyCenterIssue} objects in the Safety Center. */
154     @NonNull
getIssues()155     public List<SafetyCenterIssue> getIssues() {
156         return mIssues;
157     }
158 
159     /**
160      * Returns the structured list of {@link SafetyCenterEntry} and {@link SafetyCenterEntryGroup}
161      * objects, wrapped in {@link SafetyCenterEntryOrGroup}.
162      */
163     @NonNull
getEntriesOrGroups()164     public List<SafetyCenterEntryOrGroup> getEntriesOrGroups() {
165         return mEntriesOrGroups;
166     }
167 
168     /** Returns the list of {@link SafetyCenterStaticEntryGroup} objects in the Safety Center. */
169     @NonNull
getStaticEntryGroups()170     public List<SafetyCenterStaticEntryGroup> getStaticEntryGroups() {
171         return mStaticEntryGroups;
172     }
173 
174     /** Returns the list of dismissed {@link SafetyCenterIssue} objects in the Safety Center. */
175     @NonNull
176     @RequiresApi(UPSIDE_DOWN_CAKE)
getDismissedIssues()177     public List<SafetyCenterIssue> getDismissedIssues() {
178         if (!SdkLevel.isAtLeastU()) {
179             throw new UnsupportedOperationException(
180                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
181         }
182         return mDismissedIssues;
183     }
184 
185     /**
186      * Returns a {@link Bundle} containing additional information, {@link Bundle#EMPTY} by default.
187      *
188      * <p>Note: internal state of this {@link Bundle} is not used for {@link Object#equals} and
189      * {@link Object#hashCode} implementation of {@link SafetyCenterData}.
190      */
191     @NonNull
192     @RequiresApi(UPSIDE_DOWN_CAKE)
getExtras()193     public Bundle getExtras() {
194         if (!SdkLevel.isAtLeastU()) {
195             throw new UnsupportedOperationException(
196                     "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
197         }
198         return mExtras;
199     }
200 
201     @Override
equals(Object o)202     public boolean equals(Object o) {
203         if (this == o) return true;
204         if (!(o instanceof SafetyCenterData)) return false;
205         SafetyCenterData that = (SafetyCenterData) o;
206         return Objects.equals(mStatus, that.mStatus)
207                 && Objects.equals(mIssues, that.mIssues)
208                 && Objects.equals(mEntriesOrGroups, that.mEntriesOrGroups)
209                 && Objects.equals(mStaticEntryGroups, that.mStaticEntryGroups)
210                 && Objects.equals(mDismissedIssues, that.mDismissedIssues)
211                 && areKnownExtrasContentsEqual(mExtras, that.mExtras);
212     }
213 
214     /** We're only comparing the bundle data that we know of. */
areKnownExtrasContentsEqual( @onNull Bundle left, @NonNull Bundle right)215     private static boolean areKnownExtrasContentsEqual(
216             @NonNull Bundle left, @NonNull Bundle right) {
217         return areBundlesEqual(left, right, ISSUES_TO_GROUPS_BUNDLE_KEY)
218                 && areBundlesEqual(left, right, STATIC_ENTRIES_TO_IDS_BUNDLE_KEY);
219     }
220 
areBundlesEqual( @onNull Bundle left, @NonNull Bundle right, @NonNull String bundleKey)221     private static boolean areBundlesEqual(
222             @NonNull Bundle left, @NonNull Bundle right, @NonNull String bundleKey) {
223         Bundle leftBundle = left.getBundle(bundleKey);
224         Bundle rightBundle = right.getBundle(bundleKey);
225 
226         if (leftBundle == null && rightBundle == null) {
227             return true;
228         }
229 
230         if (leftBundle == null || rightBundle == null) {
231             return false;
232         }
233 
234         Set<String> leftKeys = leftBundle.keySet();
235         Set<String> rightKeys = rightBundle.keySet();
236 
237         if (!Objects.equals(leftKeys, rightKeys)) {
238             return false;
239         }
240 
241         for (String key : leftKeys) {
242             if (!Objects.equals(
243                     getBundleValue(leftBundle, bundleKey, key),
244                     getBundleValue(rightBundle, bundleKey, key))) {
245                 return false;
246             }
247         }
248 
249         return true;
250     }
251 
252     @Override
hashCode()253     public int hashCode() {
254         return Objects.hash(
255                 mStatus,
256                 mIssues,
257                 mEntriesOrGroups,
258                 mStaticEntryGroups,
259                 mDismissedIssues,
260                 getExtrasHash());
261     }
262 
263     /** We're only hashing bundle data that we know of. */
getExtrasHash()264     private int getExtrasHash() {
265         return Objects.hash(
266                 bundleHash(ISSUES_TO_GROUPS_BUNDLE_KEY),
267                 bundleHash(STATIC_ENTRIES_TO_IDS_BUNDLE_KEY));
268     }
269 
bundleHash(@onNull String bundleKey)270     private int bundleHash(@NonNull String bundleKey) {
271         Bundle bundle = mExtras.getBundle(bundleKey);
272         if (bundle == null) {
273             return 0;
274         }
275 
276         int hash = 0;
277         for (String key : bundle.keySet()) {
278             hash +=
279                     Objects.hashCode(key)
280                             ^ Objects.hashCode(getBundleValue(bundle, bundleKey, key));
281         }
282         return hash;
283     }
284 
285     @Override
toString()286     public String toString() {
287         return "SafetyCenterData{"
288                 + "mStatus="
289                 + mStatus
290                 + ", mIssues="
291                 + mIssues
292                 + ", mEntriesOrGroups="
293                 + mEntriesOrGroups
294                 + ", mStaticEntryGroups="
295                 + mStaticEntryGroups
296                 + ", mDismissedIssues="
297                 + mDismissedIssues
298                 + ", mExtras="
299                 + extrasToString()
300                 + '}';
301     }
302 
303     /** We're only including bundle data that we know of. */
304     @NonNull
extrasToString()305     private String extrasToString() {
306         int knownExtras = 0;
307         StringBuilder sb = new StringBuilder();
308         if (appendBundleString(sb, ISSUES_TO_GROUPS_BUNDLE_KEY)) {
309             knownExtras++;
310         }
311         if (appendBundleString(sb, STATIC_ENTRIES_TO_IDS_BUNDLE_KEY)) {
312             knownExtras++;
313         }
314 
315         boolean hasUnknownExtras = knownExtras != mExtras.keySet().size();
316         if (hasUnknownExtras) {
317             sb.append("(has unknown extras)");
318         } else if (knownExtras == 0) {
319             sb.append("(no extras)");
320         }
321 
322         return sb.toString();
323     }
324 
appendBundleString(@onNull StringBuilder sb, @NonNull String bundleKey)325     private boolean appendBundleString(@NonNull StringBuilder sb, @NonNull String bundleKey) {
326         Bundle bundle = mExtras.getBundle(bundleKey);
327         if (bundle == null) {
328             return false;
329         }
330         sb.append(bundleKey);
331         sb.append(":[");
332         for (String key : bundle.keySet()) {
333             sb.append("(key=")
334                     .append(key)
335                     .append(";value=")
336                     .append(getBundleValue(bundle, bundleKey, key))
337                     .append(")");
338         }
339         sb.append("]");
340         return true;
341     }
342 
343     @Override
describeContents()344     public int describeContents() {
345         return 0;
346     }
347 
348     @Override
writeToParcel(@onNull Parcel dest, int flags)349     public void writeToParcel(@NonNull Parcel dest, int flags) {
350         dest.writeTypedObject(mStatus, flags);
351         dest.writeTypedList(mIssues);
352         dest.writeTypedList(mEntriesOrGroups);
353         dest.writeTypedList(mStaticEntryGroups);
354         if (SdkLevel.isAtLeastU()) {
355             dest.writeTypedList(mDismissedIssues);
356             dest.writeBundle(mExtras);
357         }
358     }
359 
360     /** Builder class for {@link SafetyCenterData}. */
361     @RequiresApi(UPSIDE_DOWN_CAKE)
362     public static final class Builder {
363 
364         @NonNull private final SafetyCenterStatus mStatus;
365         @NonNull private final List<SafetyCenterIssue> mIssues = new ArrayList<>();
366         @NonNull private final List<SafetyCenterEntryOrGroup> mEntriesOrGroups = new ArrayList<>();
367 
368         @NonNull
369         private final List<SafetyCenterStaticEntryGroup> mStaticEntryGroups = new ArrayList<>();
370 
371         @NonNull private final List<SafetyCenterIssue> mDismissedIssues = new ArrayList<>();
372         @NonNull private Bundle mExtras = Bundle.EMPTY;
373 
Builder(@onNull SafetyCenterStatus status)374         public Builder(@NonNull SafetyCenterStatus status) {
375             if (!SdkLevel.isAtLeastU()) {
376                 throw new UnsupportedOperationException(
377                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
378             }
379             mStatus = requireNonNull(status);
380         }
381 
382         /** Creates a {@link Builder} with the values from the given {@link SafetyCenterData}. */
Builder(@onNull SafetyCenterData safetyCenterData)383         public Builder(@NonNull SafetyCenterData safetyCenterData) {
384             if (!SdkLevel.isAtLeastU()) {
385                 throw new UnsupportedOperationException(
386                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
387             }
388             requireNonNull(safetyCenterData);
389             mStatus = safetyCenterData.mStatus;
390             mIssues.addAll(safetyCenterData.mIssues);
391             mEntriesOrGroups.addAll(safetyCenterData.mEntriesOrGroups);
392             mStaticEntryGroups.addAll(safetyCenterData.mStaticEntryGroups);
393             mDismissedIssues.addAll(safetyCenterData.mDismissedIssues);
394             mExtras = safetyCenterData.mExtras.deepCopy();
395         }
396 
397         /** Adds data for a {@link SafetyCenterIssue} to be shown in UI. */
398         @NonNull
addIssue(@onNull SafetyCenterIssue safetyCenterIssue)399         public SafetyCenterData.Builder addIssue(@NonNull SafetyCenterIssue safetyCenterIssue) {
400             mIssues.add(requireNonNull(safetyCenterIssue));
401             return this;
402         }
403 
404         /** Adds data for a {@link SafetyCenterEntryOrGroup} to be shown in UI. */
405         @NonNull
406         @SuppressWarnings("MissingGetterMatchingBuilder") // incorrectly expects "getEntryOrGroups"
addEntryOrGroup( @onNull SafetyCenterEntryOrGroup safetyCenterEntryOrGroup)407         public SafetyCenterData.Builder addEntryOrGroup(
408                 @NonNull SafetyCenterEntryOrGroup safetyCenterEntryOrGroup) {
409             mEntriesOrGroups.add(requireNonNull(safetyCenterEntryOrGroup));
410             return this;
411         }
412 
413         /** Adds data for a {@link SafetyCenterStaticEntryGroup} to be shown in UI. */
414         @NonNull
addStaticEntryGroup( @onNull SafetyCenterStaticEntryGroup safetyCenterStaticEntryGroup)415         public SafetyCenterData.Builder addStaticEntryGroup(
416                 @NonNull SafetyCenterStaticEntryGroup safetyCenterStaticEntryGroup) {
417             mStaticEntryGroups.add(requireNonNull(safetyCenterStaticEntryGroup));
418             return this;
419         }
420 
421         /** Adds data for a dismissed {@link SafetyCenterIssue} to be shown in UI. */
422         @NonNull
addDismissedIssue( @onNull SafetyCenterIssue dismissedSafetyCenterIssue)423         public SafetyCenterData.Builder addDismissedIssue(
424                 @NonNull SafetyCenterIssue dismissedSafetyCenterIssue) {
425             mDismissedIssues.add(requireNonNull(dismissedSafetyCenterIssue));
426             return this;
427         }
428 
429         /**
430          * Sets additional information for the {@link SafetyCenterData}.
431          *
432          * <p>If not set, the default value is {@link Bundle#EMPTY}.
433          */
434         @NonNull
setExtras(@onNull Bundle extras)435         public SafetyCenterData.Builder setExtras(@NonNull Bundle extras) {
436             mExtras = requireNonNull(extras);
437             return this;
438         }
439 
440         /**
441          * Resets additional information for the {@link SafetyCenterData} to the default value of
442          * {@link Bundle#EMPTY}.
443          */
444         @NonNull
clearExtras()445         public SafetyCenterData.Builder clearExtras() {
446             mExtras = Bundle.EMPTY;
447             return this;
448         }
449 
450         /**
451          * Clears data for all the {@link SafetyCenterIssue}s that were added to this {@link
452          * SafetyCenterData.Builder}.
453          */
454         @NonNull
clearIssues()455         public SafetyCenterData.Builder clearIssues() {
456             mIssues.clear();
457             return this;
458         }
459 
460         /**
461          * Clears data for all the {@link SafetyCenterEntryOrGroup}s that were added to this {@link
462          * SafetyCenterData.Builder}.
463          */
464         @NonNull
clearEntriesOrGroups()465         public SafetyCenterData.Builder clearEntriesOrGroups() {
466             mEntriesOrGroups.clear();
467             return this;
468         }
469 
470         /**
471          * Clears data for all the {@link SafetyCenterStaticEntryGroup}s that were added to this
472          * {@link SafetyCenterData.Builder}.
473          */
474         @NonNull
clearStaticEntryGroups()475         public SafetyCenterData.Builder clearStaticEntryGroups() {
476             mStaticEntryGroups.clear();
477             return this;
478         }
479 
480         /**
481          * Clears data for all the dismissed {@link SafetyCenterIssue}s that were added to this
482          * {@link SafetyCenterData.Builder}.
483          */
484         @NonNull
clearDismissedIssues()485         public SafetyCenterData.Builder clearDismissedIssues() {
486             mDismissedIssues.clear();
487             return this;
488         }
489 
490         /**
491          * Creates the {@link SafetyCenterData} defined by this {@link SafetyCenterData.Builder}.
492          */
493         @NonNull
build()494         public SafetyCenterData build() {
495             List<SafetyCenterIssue> issues = unmodifiableList(new ArrayList<>(mIssues));
496             List<SafetyCenterEntryOrGroup> entriesOrGroups =
497                     unmodifiableList(new ArrayList<>(mEntriesOrGroups));
498             List<SafetyCenterStaticEntryGroup> staticEntryGroups =
499                     unmodifiableList(new ArrayList<>(mStaticEntryGroups));
500             List<SafetyCenterIssue> dismissedIssues =
501                     unmodifiableList(new ArrayList<>(mDismissedIssues));
502 
503             return new SafetyCenterData(
504                     mStatus, issues, entriesOrGroups, staticEntryGroups, dismissedIssues, mExtras);
505         }
506     }
507 
508     @Nullable
getBundleValue( @onNull Bundle bundle, @NonNull String bundleParentKey, @NonNull String key)509     private static Object getBundleValue(
510             @NonNull Bundle bundle, @NonNull String bundleParentKey, @NonNull String key) {
511         switch (bundleParentKey) {
512             case ISSUES_TO_GROUPS_BUNDLE_KEY:
513                 return bundle.getStringArrayList(key);
514             case STATIC_ENTRIES_TO_IDS_BUNDLE_KEY:
515                 return bundle.getString(key);
516             default:
517         }
518         throw new IllegalArgumentException("Unexpected bundle parent key: " + bundleParentKey);
519     }
520 }
521