• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         }
181         return mDismissedIssues;
182     }
183 
184     /**
185      * Returns a {@link Bundle} containing additional information, {@link Bundle#EMPTY} by default.
186      *
187      * <p>Note: internal state of this {@link Bundle} is not used for {@link Object#equals} and
188      * {@link Object#hashCode} implementation of {@link SafetyCenterData}.
189      */
190     @NonNull
191     @RequiresApi(UPSIDE_DOWN_CAKE)
getExtras()192     public Bundle getExtras() {
193         if (!SdkLevel.isAtLeastU()) {
194             throw new UnsupportedOperationException();
195         }
196         return mExtras;
197     }
198 
199     @Override
equals(Object o)200     public boolean equals(Object o) {
201         if (this == o) return true;
202         if (!(o instanceof SafetyCenterData)) return false;
203         SafetyCenterData that = (SafetyCenterData) o;
204         return Objects.equals(mStatus, that.mStatus)
205                 && Objects.equals(mIssues, that.mIssues)
206                 && Objects.equals(mEntriesOrGroups, that.mEntriesOrGroups)
207                 && Objects.equals(mStaticEntryGroups, that.mStaticEntryGroups)
208                 && Objects.equals(mDismissedIssues, that.mDismissedIssues)
209                 && areKnownExtrasContentsEqual(mExtras, that.mExtras);
210     }
211 
212     /** We're only comparing the bundle data that we know of. */
areKnownExtrasContentsEqual( @onNull Bundle left, @NonNull Bundle right)213     private static boolean areKnownExtrasContentsEqual(
214             @NonNull Bundle left, @NonNull Bundle right) {
215         return areBundlesEqual(left, right, ISSUES_TO_GROUPS_BUNDLE_KEY)
216                 && areBundlesEqual(left, right, STATIC_ENTRIES_TO_IDS_BUNDLE_KEY);
217     }
218 
areBundlesEqual( @onNull Bundle left, @NonNull Bundle right, @NonNull String bundleKey)219     private static boolean areBundlesEqual(
220             @NonNull Bundle left, @NonNull Bundle right, @NonNull String bundleKey) {
221         Bundle leftBundle = left.getBundle(bundleKey);
222         Bundle rightBundle = right.getBundle(bundleKey);
223 
224         if (leftBundle == null && rightBundle == null) {
225             return true;
226         }
227 
228         if (leftBundle == null || rightBundle == null) {
229             return false;
230         }
231 
232         Set<String> leftKeys = leftBundle.keySet();
233         Set<String> rightKeys = rightBundle.keySet();
234 
235         if (!Objects.equals(leftKeys, rightKeys)) {
236             return false;
237         }
238 
239         for (String key : leftKeys) {
240             if (!Objects.equals(
241                     getBundleValue(leftBundle, bundleKey, key),
242                     getBundleValue(rightBundle, bundleKey, key))) {
243                 return false;
244             }
245         }
246 
247         return true;
248     }
249 
250     @Override
hashCode()251     public int hashCode() {
252         return Objects.hash(
253                 mStatus,
254                 mIssues,
255                 mEntriesOrGroups,
256                 mStaticEntryGroups,
257                 mDismissedIssues,
258                 getExtrasHash());
259     }
260 
261     /** We're only hashing bundle data that we know of. */
getExtrasHash()262     private int getExtrasHash() {
263         return Objects.hash(
264                 bundleHash(ISSUES_TO_GROUPS_BUNDLE_KEY),
265                 bundleHash(STATIC_ENTRIES_TO_IDS_BUNDLE_KEY));
266     }
267 
bundleHash(@onNull String bundleKey)268     private int bundleHash(@NonNull String bundleKey) {
269         Bundle bundle = mExtras.getBundle(bundleKey);
270         if (bundle == null) {
271             return 0;
272         }
273 
274         int hash = 0;
275         for (String key : bundle.keySet()) {
276             hash +=
277                     Objects.hashCode(key)
278                             ^ Objects.hashCode(getBundleValue(bundle, bundleKey, key));
279         }
280         return hash;
281     }
282 
283     @Override
toString()284     public String toString() {
285         return "SafetyCenterData{"
286                 + "mStatus="
287                 + mStatus
288                 + ", mIssues="
289                 + mIssues
290                 + ", mEntriesOrGroups="
291                 + mEntriesOrGroups
292                 + ", mStaticEntryGroups="
293                 + mStaticEntryGroups
294                 + ", mDismissedIssues="
295                 + mDismissedIssues
296                 + ", mExtras="
297                 + extrasToString()
298                 + '}';
299     }
300 
301     /** We're only including bundle data that we know of. */
302     @NonNull
extrasToString()303     private String extrasToString() {
304         int knownExtras = 0;
305         StringBuilder sb = new StringBuilder();
306         if (appendBundleString(sb, ISSUES_TO_GROUPS_BUNDLE_KEY)) {
307             knownExtras++;
308         }
309         if (appendBundleString(sb, STATIC_ENTRIES_TO_IDS_BUNDLE_KEY)) {
310             knownExtras++;
311         }
312 
313         boolean hasUnknownExtras = knownExtras != mExtras.keySet().size();
314         if (hasUnknownExtras) {
315             sb.append("(has unknown extras)");
316         } else if (knownExtras == 0) {
317             sb.append("(no extras)");
318         }
319 
320         return sb.toString();
321     }
322 
appendBundleString(@onNull StringBuilder sb, @NonNull String bundleKey)323     private boolean appendBundleString(@NonNull StringBuilder sb, @NonNull String bundleKey) {
324         Bundle bundle = mExtras.getBundle(bundleKey);
325         if (bundle == null) {
326             return false;
327         }
328         sb.append(bundleKey);
329         sb.append(":[");
330         for (String key : bundle.keySet()) {
331             sb.append("(key=")
332                     .append(key)
333                     .append(";value=")
334                     .append(getBundleValue(bundle, bundleKey, key))
335                     .append(")");
336         }
337         sb.append("]");
338         return true;
339     }
340 
341     @Override
describeContents()342     public int describeContents() {
343         return 0;
344     }
345 
346     @Override
writeToParcel(@onNull Parcel dest, int flags)347     public void writeToParcel(@NonNull Parcel dest, int flags) {
348         dest.writeTypedObject(mStatus, flags);
349         dest.writeTypedList(mIssues);
350         dest.writeTypedList(mEntriesOrGroups);
351         dest.writeTypedList(mStaticEntryGroups);
352         if (SdkLevel.isAtLeastU()) {
353             dest.writeTypedList(mDismissedIssues);
354             dest.writeBundle(mExtras);
355         }
356     }
357 
358     /** Builder class for {@link SafetyCenterData}. */
359     @RequiresApi(UPSIDE_DOWN_CAKE)
360     public static final class Builder {
361 
362         @NonNull private final SafetyCenterStatus mStatus;
363         @NonNull private final List<SafetyCenterIssue> mIssues = new ArrayList<>();
364         @NonNull private final List<SafetyCenterEntryOrGroup> mEntriesOrGroups = new ArrayList<>();
365 
366         @NonNull
367         private final List<SafetyCenterStaticEntryGroup> mStaticEntryGroups = new ArrayList<>();
368 
369         @NonNull private final List<SafetyCenterIssue> mDismissedIssues = new ArrayList<>();
370         @NonNull private Bundle mExtras = Bundle.EMPTY;
371 
Builder(@onNull SafetyCenterStatus status)372         public Builder(@NonNull SafetyCenterStatus status) {
373             if (!SdkLevel.isAtLeastU()) {
374                 throw new UnsupportedOperationException();
375             }
376             mStatus = requireNonNull(status);
377         }
378 
379         /** Creates a {@link Builder} with the values from the given {@link SafetyCenterData}. */
Builder(@onNull SafetyCenterData safetyCenterData)380         public Builder(@NonNull SafetyCenterData safetyCenterData) {
381             if (!SdkLevel.isAtLeastU()) {
382                 throw new UnsupportedOperationException();
383             }
384             requireNonNull(safetyCenterData);
385             mStatus = safetyCenterData.mStatus;
386             mIssues.addAll(safetyCenterData.mIssues);
387             mEntriesOrGroups.addAll(safetyCenterData.mEntriesOrGroups);
388             mStaticEntryGroups.addAll(safetyCenterData.mStaticEntryGroups);
389             mDismissedIssues.addAll(safetyCenterData.mDismissedIssues);
390             mExtras = safetyCenterData.mExtras.deepCopy();
391         }
392 
393         /** Adds data for a {@link SafetyCenterIssue} to be shown in UI. */
394         @NonNull
addIssue(@onNull SafetyCenterIssue safetyCenterIssue)395         public SafetyCenterData.Builder addIssue(@NonNull SafetyCenterIssue safetyCenterIssue) {
396             mIssues.add(requireNonNull(safetyCenterIssue));
397             return this;
398         }
399 
400         /** Adds data for a {@link SafetyCenterEntryOrGroup} to be shown in UI. */
401         @NonNull
402         @SuppressWarnings("MissingGetterMatchingBuilder") // incorrectly expects "getEntryOrGroups"
addEntryOrGroup( @onNull SafetyCenterEntryOrGroup safetyCenterEntryOrGroup)403         public SafetyCenterData.Builder addEntryOrGroup(
404                 @NonNull SafetyCenterEntryOrGroup safetyCenterEntryOrGroup) {
405             mEntriesOrGroups.add(requireNonNull(safetyCenterEntryOrGroup));
406             return this;
407         }
408 
409         /** Adds data for a {@link SafetyCenterStaticEntryGroup} to be shown in UI. */
410         @NonNull
addStaticEntryGroup( @onNull SafetyCenterStaticEntryGroup safetyCenterStaticEntryGroup)411         public SafetyCenterData.Builder addStaticEntryGroup(
412                 @NonNull SafetyCenterStaticEntryGroup safetyCenterStaticEntryGroup) {
413             mStaticEntryGroups.add(requireNonNull(safetyCenterStaticEntryGroup));
414             return this;
415         }
416 
417         /** Adds data for a dismissed {@link SafetyCenterIssue} to be shown in UI. */
418         @NonNull
addDismissedIssue( @onNull SafetyCenterIssue dismissedSafetyCenterIssue)419         public SafetyCenterData.Builder addDismissedIssue(
420                 @NonNull SafetyCenterIssue dismissedSafetyCenterIssue) {
421             mDismissedIssues.add(requireNonNull(dismissedSafetyCenterIssue));
422             return this;
423         }
424 
425         /**
426          * Sets additional information for the {@link SafetyCenterData}.
427          *
428          * <p>If not set, the default value is {@link Bundle#EMPTY}.
429          */
430         @NonNull
setExtras(@onNull Bundle extras)431         public SafetyCenterData.Builder setExtras(@NonNull Bundle extras) {
432             mExtras = requireNonNull(extras);
433             return this;
434         }
435 
436         /**
437          * Resets additional information for the {@link SafetyCenterData} to the default value of
438          * {@link Bundle#EMPTY}.
439          */
440         @NonNull
clearExtras()441         public SafetyCenterData.Builder clearExtras() {
442             mExtras = Bundle.EMPTY;
443             return this;
444         }
445 
446         /**
447          * Clears data for all the {@link SafetyCenterIssue}s that were added to this {@link
448          * SafetyCenterData.Builder}.
449          */
450         @NonNull
clearIssues()451         public SafetyCenterData.Builder clearIssues() {
452             mIssues.clear();
453             return this;
454         }
455 
456         /**
457          * Clears data for all the {@link SafetyCenterEntryOrGroup}s that were added to this {@link
458          * SafetyCenterData.Builder}.
459          */
460         @NonNull
clearEntriesOrGroups()461         public SafetyCenterData.Builder clearEntriesOrGroups() {
462             mEntriesOrGroups.clear();
463             return this;
464         }
465 
466         /**
467          * Clears data for all the {@link SafetyCenterStaticEntryGroup}s that were added to this
468          * {@link SafetyCenterData.Builder}.
469          */
470         @NonNull
clearStaticEntryGroups()471         public SafetyCenterData.Builder clearStaticEntryGroups() {
472             mStaticEntryGroups.clear();
473             return this;
474         }
475 
476         /**
477          * Clears data for all the dismissed {@link SafetyCenterIssue}s that were added to this
478          * {@link SafetyCenterData.Builder}.
479          */
480         @NonNull
clearDismissedIssues()481         public SafetyCenterData.Builder clearDismissedIssues() {
482             mDismissedIssues.clear();
483             return this;
484         }
485 
486         /**
487          * Creates the {@link SafetyCenterData} defined by this {@link SafetyCenterData.Builder}.
488          */
489         @NonNull
build()490         public SafetyCenterData build() {
491             List<SafetyCenterIssue> issues = unmodifiableList(new ArrayList<>(mIssues));
492             List<SafetyCenterEntryOrGroup> entriesOrGroups =
493                     unmodifiableList(new ArrayList<>(mEntriesOrGroups));
494             List<SafetyCenterStaticEntryGroup> staticEntryGroups =
495                     unmodifiableList(new ArrayList<>(mStaticEntryGroups));
496             List<SafetyCenterIssue> dismissedIssues =
497                     unmodifiableList(new ArrayList<>(mDismissedIssues));
498 
499             return new SafetyCenterData(
500                     mStatus, issues, entriesOrGroups, staticEntryGroups, dismissedIssues, mExtras);
501         }
502     }
503 
504     @Nullable
getBundleValue( @onNull Bundle bundle, @NonNull String bundleParentKey, @NonNull String key)505     private static Object getBundleValue(
506             @NonNull Bundle bundle, @NonNull String bundleParentKey, @NonNull String key) {
507         switch (bundleParentKey) {
508             case ISSUES_TO_GROUPS_BUNDLE_KEY:
509                 return bundle.getStringArrayList(key);
510             case STATIC_ENTRIES_TO_IDS_BUNDLE_KEY:
511                 return bundle.getString(key);
512             default:
513         }
514         throw new IllegalArgumentException("Unexpected bundle parent key: " + bundleParentKey);
515     }
516 }
517