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.config;
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.IntDef;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.StringRes;
29 import android.annotation.SuppressLint;
30 import android.annotation.SystemApi;
31 import android.content.res.Resources;
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.List;
43 import java.util.Objects;
44 
45 /**
46  * Data class used to represent the initial configuration of a group of safety sources.
47  *
48  * @hide
49  */
50 @SystemApi
51 @RequiresApi(TIRAMISU)
52 public final class SafetySourcesGroup implements Parcelable {
53 
54     /**
55      * Indicates that the safety sources group should be displayed as a collapsible group with an
56      * icon (stateless or stateful) and an optional default summary.
57      *
58      * @deprecated use {@link #SAFETY_SOURCES_GROUP_TYPE_STATEFUL} instead.
59      */
60     public static final int SAFETY_SOURCES_GROUP_TYPE_COLLAPSIBLE = 0;
61 
62     /**
63      * Indicates that the safety sources group should be displayed as a group that may contribute to
64      * the overall Safety Center status. This is indicated by a group stateful icon. If all sources
65      * in the group have an unspecified status then a stateless group icon might be applied.
66      */
67     public static final int SAFETY_SOURCES_GROUP_TYPE_STATEFUL =
68             SAFETY_SOURCES_GROUP_TYPE_COLLAPSIBLE;
69 
70     /**
71      * Indicates that the safety sources group should be displayed as a rigid group with no icon and
72      * no summary.
73      *
74      * @deprecated use {@link #SAFETY_SOURCES_GROUP_TYPE_STATELESS} instead.
75      */
76     public static final int SAFETY_SOURCES_GROUP_TYPE_RIGID = 1;
77 
78     /**
79      * Indicates that the safety sources group should be displayed as a group that does not
80      * contribute to the overall Safety Center status. All sources of type dynamic in the group can
81      * only report an unspecified status. The stateless icon and summary may be ignored and not be
82      * displayed.
83      */
84     public static final int SAFETY_SOURCES_GROUP_TYPE_STATELESS = SAFETY_SOURCES_GROUP_TYPE_RIGID;
85 
86     /**
87      * Indicates that the safety sources group should not be displayed. All sources in the group
88      * must be of type issue-only.
89      */
90     public static final int SAFETY_SOURCES_GROUP_TYPE_HIDDEN = 2;
91 
92     /**
93      * All possible types for a safety sources group.
94      *
95      * @hide
96      */
97     @SuppressLint("UniqueConstants") // Intentionally renaming the COLLAPSIBLE and RIGID constants.
98     @Retention(RetentionPolicy.SOURCE)
99     @IntDef(
100             prefix = "SAFETY_SOURCES_GROUP_TYPE_",
101             value = {
102                 SAFETY_SOURCES_GROUP_TYPE_COLLAPSIBLE,
103                 SAFETY_SOURCES_GROUP_TYPE_STATEFUL,
104                 SAFETY_SOURCES_GROUP_TYPE_RIGID,
105                 SAFETY_SOURCES_GROUP_TYPE_STATELESS,
106                 SAFETY_SOURCES_GROUP_TYPE_HIDDEN
107             })
108     public @interface SafetySourceGroupType {}
109 
110     /**
111      * Indicates that no special icon will be displayed by a safety sources group when all the
112      * sources contained in it are stateless.
113      */
114     public static final int STATELESS_ICON_TYPE_NONE = 0;
115 
116     /**
117      * Indicates that the privacy icon will be displayed by a safety sources group when all the
118      * sources contained in it are stateless.
119      */
120     public static final int STATELESS_ICON_TYPE_PRIVACY = 1;
121 
122     /**
123      * All possible stateless icon types for a safety sources group.
124      *
125      * @hide
126      */
127     @Retention(RetentionPolicy.SOURCE)
128     @IntDef(
129             prefix = "STATELESS_ICON_TYPE_",
130             value = {STATELESS_ICON_TYPE_NONE, STATELESS_ICON_TYPE_PRIVACY})
131     public @interface StatelessIconType {}
132 
133     @NonNull
134     public static final Creator<SafetySourcesGroup> CREATOR =
135             new Creator<SafetySourcesGroup>() {
136                 @Override
137                 public SafetySourcesGroup createFromParcel(Parcel in) {
138                     Builder builder =
139                             new Builder()
140                                     .setId(in.readString())
141                                     .setTitleResId(in.readInt())
142                                     .setSummaryResId(in.readInt())
143                                     .setStatelessIconType(in.readInt());
144                     List<SafetySource> safetySources =
145                             requireNonNull(in.createTypedArrayList(SafetySource.CREATOR));
146                     for (int i = 0; i < safetySources.size(); i++) {
147                         builder.addSafetySource(safetySources.get(i));
148                     }
149                     if (SdkLevel.isAtLeastU()) {
150                         builder.setType(in.readInt());
151                     }
152                     return builder.build();
153                 }
154 
155                 @Override
156                 public SafetySourcesGroup[] newArray(int size) {
157                     return new SafetySourcesGroup[size];
158                 }
159             };
160 
161     @SafetySourceGroupType private final int mType;
162     @NonNull private final String mId;
163     @StringRes private final int mTitleResId;
164     @StringRes private final int mSummaryResId;
165     @StatelessIconType private final int mStatelessIconType;
166     @NonNull private final List<SafetySource> mSafetySources;
167 
SafetySourcesGroup( @afetySourceGroupType int type, @NonNull String id, @StringRes int titleResId, @StringRes int summaryResId, @StatelessIconType int statelessIconType, @NonNull List<SafetySource> safetySources)168     private SafetySourcesGroup(
169             @SafetySourceGroupType int type,
170             @NonNull String id,
171             @StringRes int titleResId,
172             @StringRes int summaryResId,
173             @StatelessIconType int statelessIconType,
174             @NonNull List<SafetySource> safetySources) {
175         mType = type;
176         mId = id;
177         mTitleResId = titleResId;
178         mSummaryResId = summaryResId;
179         mStatelessIconType = statelessIconType;
180         mSafetySources = safetySources;
181     }
182 
183     /** Returns the type of this safety sources group. */
184     @SafetySourceGroupType
getType()185     public int getType() {
186         return mType;
187     }
188 
189     /**
190      * Returns the id of this safety sources group.
191      *
192      * <p>The id is unique among safety sources groups in a Safety Center configuration.
193      */
194     @NonNull
getId()195     public String getId() {
196         return mId;
197     }
198 
199     /**
200      * Returns the resource id of the title of this safety sources group.
201      *
202      * <p>The id refers to a string resource that is either accessible from any resource context or
203      * that is accessible from the same resource context that was used to load the Safety Center
204      * configuration. The id is {@link Resources#ID_NULL} when a title is not provided.
205      */
206     @StringRes
getTitleResId()207     public int getTitleResId() {
208         return mTitleResId;
209     }
210 
211     /**
212      * Returns the resource id of the summary of this safety sources group.
213      *
214      * <p>The id refers to a string resource that is either accessible from any resource context or
215      * that is accessible from the same resource context that was used to load the Safety Center
216      * configuration. The id is {@link Resources#ID_NULL} when a summary is not provided.
217      */
218     @StringRes
getSummaryResId()219     public int getSummaryResId() {
220         return mSummaryResId;
221     }
222 
223     /**
224      * Returns the stateless icon type of this safety sources group.
225      *
226      * <p>If set to a value other than {@link SafetySourcesGroup#STATELESS_ICON_TYPE_NONE}, the icon
227      * specified will be displayed for collapsible groups when all the sources contained in the
228      * group are stateless.
229      */
230     @StatelessIconType
getStatelessIconType()231     public int getStatelessIconType() {
232         return mStatelessIconType;
233     }
234 
235     /**
236      * Returns the list of {@link SafetySource}s in this safety sources group.
237      *
238      * <p>A safety sources group contains at least one {@link SafetySource}.
239      */
240     @NonNull
getSafetySources()241     public List<SafetySource> getSafetySources() {
242         return mSafetySources;
243     }
244 
245     @Override
equals(Object o)246     public boolean equals(Object o) {
247         if (this == o) return true;
248         if (!(o instanceof SafetySourcesGroup)) return false;
249         SafetySourcesGroup that = (SafetySourcesGroup) o;
250         return mType == that.mType
251                 && Objects.equals(mId, that.mId)
252                 && mTitleResId == that.mTitleResId
253                 && mSummaryResId == that.mSummaryResId
254                 && mStatelessIconType == that.mStatelessIconType
255                 && Objects.equals(mSafetySources, that.mSafetySources);
256     }
257 
258     @Override
hashCode()259     public int hashCode() {
260         return Objects.hash(
261                 mType, mId, mTitleResId, mSummaryResId, mStatelessIconType, mSafetySources);
262     }
263 
264     @Override
toString()265     public String toString() {
266         return "SafetySourcesGroup{"
267                 + "mType="
268                 + mType
269                 + ", mId="
270                 + mId
271                 + ", mTitleResId="
272                 + mTitleResId
273                 + ", mSummaryResId="
274                 + mSummaryResId
275                 + ", mStatelessIconType="
276                 + mStatelessIconType
277                 + ", mSafetySources="
278                 + mSafetySources
279                 + '}';
280     }
281 
282     @Override
describeContents()283     public int describeContents() {
284         return 0;
285     }
286 
287     @Override
writeToParcel(@onNull Parcel dest, int flags)288     public void writeToParcel(@NonNull Parcel dest, int flags) {
289         dest.writeString(mId);
290         dest.writeInt(mTitleResId);
291         dest.writeInt(mSummaryResId);
292         dest.writeInt(mStatelessIconType);
293         dest.writeTypedList(mSafetySources);
294         if (SdkLevel.isAtLeastU()) {
295             dest.writeInt(mType);
296         }
297     }
298 
299     /** Builder class for {@link SafetySourcesGroup}. */
300     public static final class Builder {
301 
302         private final List<SafetySource> mSafetySources = new ArrayList<>();
303 
304         @Nullable @SafetySourceGroupType private Integer mType;
305         @Nullable private String mId;
306         @Nullable @StringRes private Integer mTitleResId;
307         @Nullable @StringRes private Integer mSummaryResId;
308         @Nullable @StatelessIconType private Integer mStatelessIconType;
309 
310         /** Creates a {@link Builder} for a {@link SafetySourcesGroup}. */
Builder()311         public Builder() {}
312 
313         /** Creates a {@link Builder} with the values from the given {@link SafetySourcesGroup}. */
314         @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull SafetySourcesGroup original)315         public Builder(@NonNull SafetySourcesGroup original) {
316             if (!SdkLevel.isAtLeastU()) {
317                 throw new UnsupportedOperationException(
318                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
319             }
320             requireNonNull(original);
321             mSafetySources.addAll(original.mSafetySources);
322             mType = original.mType;
323             mId = original.mId;
324             mTitleResId = original.mTitleResId;
325             mSummaryResId = original.mSummaryResId;
326             mStatelessIconType = original.mStatelessIconType;
327         }
328 
329         /**
330          * Sets the type of this safety sources group.
331          *
332          * <p>If the type is not explicitly set, the type is inferred according to the state of
333          * certain fields. If no title is provided when building the group, the group is of type
334          * hidden. If a title is provided but no summary or stateless icon are provided when
335          * building the group, the group is of type stateless. Otherwise, the group is of type
336          * stateful.
337          */
338         @NonNull
339         @RequiresApi(UPSIDE_DOWN_CAKE)
setType(@afetySourceGroupType int type)340         public Builder setType(@SafetySourceGroupType int type) {
341             mType = type;
342             return this;
343         }
344 
345         /**
346          * Sets the id of this safety sources group.
347          *
348          * <p>The id must be unique among safety sources groups in a Safety Center configuration.
349          */
350         @NonNull
setId(@ullable String id)351         public Builder setId(@Nullable String id) {
352             mId = id;
353             return this;
354         }
355 
356         /**
357          * Sets the resource id of the title of this safety sources group.
358          *
359          * <p>The id must refer to a string resource that is either accessible from any resource
360          * context or that is accessible from the same resource context that was used to load the
361          * Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when a title is
362          * not provided. A title is required unless the group only contains safety sources of type
363          * issue only.
364          */
365         @NonNull
setTitleResId(@tringRes int titleResId)366         public Builder setTitleResId(@StringRes int titleResId) {
367             mTitleResId = titleResId;
368             return this;
369         }
370 
371         /**
372          * Sets the resource id of the summary of this safety sources group.
373          *
374          * <p>The id must refer to a string resource that is either accessible from any resource
375          * context or that is accessible from the same resource context that was used to load the
376          * Safety Center configuration. The id defaults to {@link Resources#ID_NULL} when a summary
377          * is not provided.
378          */
379         @NonNull
setSummaryResId(@tringRes int summaryResId)380         public Builder setSummaryResId(@StringRes int summaryResId) {
381             mSummaryResId = summaryResId;
382             return this;
383         }
384 
385         /**
386          * Sets the stateless icon type of this safety sources group.
387          *
388          * <p>If set to a value other than {@link SafetySourcesGroup#STATELESS_ICON_TYPE_NONE}, the
389          * icon specified will be displayed for collapsible groups when all the sources contained in
390          * the group are stateless.
391          */
392         @NonNull
setStatelessIconType(@tatelessIconType int statelessIconType)393         public Builder setStatelessIconType(@StatelessIconType int statelessIconType) {
394             mStatelessIconType = statelessIconType;
395             return this;
396         }
397 
398         /**
399          * Adds a {@link SafetySource} to this safety sources group.
400          *
401          * <p>A safety sources group must contain at least one {@link SafetySource}.
402          */
403         @NonNull
addSafetySource(@onNull SafetySource safetySource)404         public Builder addSafetySource(@NonNull SafetySource safetySource) {
405             mSafetySources.add(requireNonNull(safetySource));
406             return this;
407         }
408 
409         /**
410          * Creates the {@link SafetySourcesGroup} defined by this {@link Builder}.
411          *
412          * @throws IllegalStateException if any constraint on the safety sources group is violated
413          */
414         @NonNull
build()415         public SafetySourcesGroup build() {
416             String id = mId;
417             BuilderUtils.validateId(id, "id", true, false);
418 
419             List<SafetySource> safetySources = unmodifiableList(new ArrayList<>(mSafetySources));
420             if (safetySources.isEmpty()) {
421                 throw new IllegalStateException("Safety sources group empty");
422             }
423 
424             int summaryResId = BuilderUtils.validateResId(mSummaryResId, "summary", false, false);
425 
426             int statelessIconType =
427                     BuilderUtils.validateIntDef(
428                             mStatelessIconType,
429                             "statelessIconType",
430                             false,
431                             false,
432                             STATELESS_ICON_TYPE_NONE,
433                             STATELESS_ICON_TYPE_NONE,
434                             STATELESS_ICON_TYPE_PRIVACY);
435 
436             boolean hasOnlyIssueOnlySources = true;
437             int safetySourcesSize = safetySources.size();
438             for (int i = 0; i < safetySourcesSize; i++) {
439                 int type = safetySources.get(i).getType();
440                 if (type != SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY) {
441                     hasOnlyIssueOnlySources = false;
442                     break;
443                 }
444             }
445 
446             int inferredGroupType = SAFETY_SOURCES_GROUP_TYPE_STATELESS;
447             if (hasOnlyIssueOnlySources) {
448                 inferredGroupType = SAFETY_SOURCES_GROUP_TYPE_HIDDEN;
449             } else if (summaryResId != Resources.ID_NULL
450                     || statelessIconType != STATELESS_ICON_TYPE_NONE) {
451                 inferredGroupType = SAFETY_SOURCES_GROUP_TYPE_STATEFUL;
452             }
453             int type =
454                     BuilderUtils.validateIntDef(
455                             mType,
456                             "type",
457                             false,
458                             false,
459                             inferredGroupType,
460                             SAFETY_SOURCES_GROUP_TYPE_STATEFUL,
461                             SAFETY_SOURCES_GROUP_TYPE_STATELESS,
462                             SAFETY_SOURCES_GROUP_TYPE_HIDDEN);
463             if (type == SAFETY_SOURCES_GROUP_TYPE_HIDDEN && !hasOnlyIssueOnlySources) {
464                 throw new IllegalStateException(
465                         "Safety sources groups of type hidden can only contain sources of type "
466                                 + "issue-only");
467             }
468             if (type != SAFETY_SOURCES_GROUP_TYPE_HIDDEN && hasOnlyIssueOnlySources) {
469                 throw new IllegalStateException(
470                         "Safety sources groups containing only sources of type issue-only must be "
471                                 + "of type hidden");
472             }
473 
474             boolean isStateful = type == SAFETY_SOURCES_GROUP_TYPE_STATEFUL;
475             boolean isStateless = type == SAFETY_SOURCES_GROUP_TYPE_STATELESS;
476             int titleResId =
477                     BuilderUtils.validateResId(
478                             mTitleResId, "title", isStateful || isStateless, false);
479 
480             return new SafetySourcesGroup(
481                     type, id, titleResId, summaryResId, statelessIconType, safetySources);
482         }
483     }
484 }
485