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.Objects.requireNonNull;
25 
26 import android.annotation.IntDef;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.annotation.SystemApi;
30 import android.app.PendingIntent;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.text.TextUtils;
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.Objects;
42 
43 /**
44  * Data for a safety source status in the Safety Center page, which conveys the overall state of the
45  * safety source and allows a user to navigate to the source.
46  *
47  * @hide
48  */
49 @SystemApi
50 @RequiresApi(TIRAMISU)
51 public final class SafetySourceStatus implements Parcelable {
52 
53     @NonNull
54     public static final Creator<SafetySourceStatus> CREATOR =
55             new Creator<SafetySourceStatus>() {
56                 @Override
57                 public SafetySourceStatus createFromParcel(Parcel in) {
58                     CharSequence title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
59                     CharSequence summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
60                     int severityLevel = in.readInt();
61                     return new Builder(title, summary, severityLevel)
62                             .setPendingIntent(in.readTypedObject(PendingIntent.CREATOR))
63                             .setIconAction(in.readTypedObject(IconAction.CREATOR))
64                             .setEnabled(in.readBoolean())
65                             .build();
66                 }
67 
68                 @Override
69                 public SafetySourceStatus[] newArray(int size) {
70                     return new SafetySourceStatus[size];
71                 }
72             };
73 
74     @NonNull private final CharSequence mTitle;
75     @NonNull private final CharSequence mSummary;
76     @SafetySourceData.SeverityLevel private final int mSeverityLevel;
77     @Nullable private final PendingIntent mPendingIntent;
78     @Nullable private final IconAction mIconAction;
79     private final boolean mEnabled;
80 
SafetySourceStatus( @onNull CharSequence title, @NonNull CharSequence summary, @SafetySourceData.SeverityLevel int severityLevel, @Nullable PendingIntent pendingIntent, @Nullable IconAction iconAction, boolean enabled)81     private SafetySourceStatus(
82             @NonNull CharSequence title,
83             @NonNull CharSequence summary,
84             @SafetySourceData.SeverityLevel int severityLevel,
85             @Nullable PendingIntent pendingIntent,
86             @Nullable IconAction iconAction,
87             boolean enabled) {
88         this.mTitle = title;
89         this.mSummary = summary;
90         this.mSeverityLevel = severityLevel;
91         this.mPendingIntent = pendingIntent;
92         this.mIconAction = iconAction;
93         this.mEnabled = enabled;
94     }
95 
96     /** Returns the localized title of the safety source status to be displayed in the UI. */
97     @NonNull
getTitle()98     public CharSequence getTitle() {
99         return mTitle;
100     }
101 
102     /** Returns the localized summary of the safety source status to be displayed in the UI. */
103     @NonNull
getSummary()104     public CharSequence getSummary() {
105         return mSummary;
106     }
107 
108     /** Returns the {@link SafetySourceData.SeverityLevel} of the status. */
109     @SafetySourceData.SeverityLevel
getSeverityLevel()110     public int getSeverityLevel() {
111         return mSeverityLevel;
112     }
113 
114     /**
115      * Returns an optional {@link PendingIntent} that will start an activity when the safety source
116      * status UI is clicked on.
117      *
118      * <p>The action contained in the {@link PendingIntent} must start an activity.
119      *
120      * <p>If {@code null} the intent action defined in the Safety Center configuration will be
121      * invoked when the safety source status UI is clicked on. If the intent action is undefined or
122      * disabled the source is considered as disabled.
123      */
124     @Nullable
getPendingIntent()125     public PendingIntent getPendingIntent() {
126         return mPendingIntent;
127     }
128 
129     /**
130      * Returns an optional {@link IconAction} to be displayed in the safety source status UI.
131      *
132      * <p>The icon action will be a clickable icon which performs an action as indicated by the
133      * icon.
134      */
135     @Nullable
getIconAction()136     public IconAction getIconAction() {
137         return mIconAction;
138     }
139 
140     /**
141      * Returns whether the safety source status is enabled.
142      *
143      * <p>A safety source status should be disabled if it is currently unavailable on the device
144      *
145      * <p>If disabled, the status will show as grayed out in the UI, and interactions with it may be
146      * limited.
147      */
isEnabled()148     public boolean isEnabled() {
149         return mEnabled;
150     }
151 
152     @Override
describeContents()153     public int describeContents() {
154         return 0;
155     }
156 
157     @Override
writeToParcel(@onNull Parcel dest, int flags)158     public void writeToParcel(@NonNull Parcel dest, int flags) {
159         TextUtils.writeToParcel(mTitle, dest, flags);
160         TextUtils.writeToParcel(mSummary, dest, flags);
161         dest.writeInt(mSeverityLevel);
162         dest.writeTypedObject(mPendingIntent, flags);
163         dest.writeTypedObject(mIconAction, flags);
164         dest.writeBoolean(mEnabled);
165     }
166 
167     @Override
equals(Object o)168     public boolean equals(Object o) {
169         if (this == o) return true;
170         if (!(o instanceof SafetySourceStatus)) return false;
171         SafetySourceStatus that = (SafetySourceStatus) o;
172         return mSeverityLevel == that.mSeverityLevel
173                 && mEnabled == that.mEnabled
174                 && TextUtils.equals(mTitle, that.mTitle)
175                 && TextUtils.equals(mSummary, that.mSummary)
176                 && Objects.equals(mPendingIntent, that.mPendingIntent)
177                 && Objects.equals(mIconAction, that.mIconAction);
178     }
179 
180     @Override
hashCode()181     public int hashCode() {
182         return Objects.hash(
183                 mTitle, mSummary, mSeverityLevel, mPendingIntent, mIconAction, mEnabled);
184     }
185 
186     @Override
toString()187     public String toString() {
188         return "SafetySourceStatus{"
189                 + "mTitle="
190                 + mTitle
191                 + ", mSummary="
192                 + mSummary
193                 + ", mSeverityLevel="
194                 + mSeverityLevel
195                 + ", mPendingIntent="
196                 + mPendingIntent
197                 + ", mIconAction="
198                 + mIconAction
199                 + ", mEnabled="
200                 + mEnabled
201                 + '}';
202     }
203 
204     /**
205      * Data for an action supported from a safety source status {@link SafetySourceStatus} in the
206      * Safety Center page.
207      *
208      * <p>The purpose of the action is to add a surface to allow the user to perform an action
209      * relating to the safety source status.
210      *
211      * <p>The action will be shown as a clickable icon chosen from a predefined set of icons (see
212      * {@link IconType}). The icon should indicate to the user what action will be performed on
213      * clicking on it.
214      */
215     public static final class IconAction implements Parcelable {
216 
217         @NonNull
218         public static final Creator<IconAction> CREATOR =
219                 new Creator<IconAction>() {
220                     @Override
221                     public IconAction createFromParcel(Parcel in) {
222                         int iconType = in.readInt();
223                         PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
224                         return new IconAction(iconType, pendingIntent);
225                     }
226 
227                     @Override
228                     public IconAction[] newArray(int size) {
229                         return new IconAction[size];
230                     }
231                 };
232 
233         /** Indicates a gear (cog) icon. */
234         public static final int ICON_TYPE_GEAR = 100;
235 
236         /** Indicates an information icon. */
237         public static final int ICON_TYPE_INFO = 200;
238 
239         /**
240          * All possible icons which can be displayed in an {@link IconAction}.
241          *
242          * @hide
243          */
244         @IntDef(
245                 prefix = {"ICON_TYPE_"},
246                 value = {
247                     ICON_TYPE_GEAR,
248                     ICON_TYPE_INFO,
249                 })
250         @Retention(RetentionPolicy.SOURCE)
251         public @interface IconType {}
252 
253         @IconType private final int mIconType;
254         @NonNull private final PendingIntent mPendingIntent;
255 
IconAction(@conType int iconType, @NonNull PendingIntent pendingIntent)256         public IconAction(@IconType int iconType, @NonNull PendingIntent pendingIntent) {
257             this.mIconType = validateIconType(iconType);
258             this.mPendingIntent = requireNonNull(pendingIntent);
259         }
260 
261         /**
262          * Returns the type of icon to be displayed in the UI.
263          *
264          * <p>The icon type should indicate what action will be performed if when invoked.
265          */
266         @IconType
getIconType()267         public int getIconType() {
268             return mIconType;
269         }
270 
271         /**
272          * Returns a {@link PendingIntent} that will start an activity when the icon action is
273          * clicked on.
274          */
275         @NonNull
getPendingIntent()276         public PendingIntent getPendingIntent() {
277             return mPendingIntent;
278         }
279 
280         @Override
describeContents()281         public int describeContents() {
282             return 0;
283         }
284 
285         @Override
writeToParcel(@onNull Parcel dest, int flags)286         public void writeToParcel(@NonNull Parcel dest, int flags) {
287             dest.writeInt(mIconType);
288             dest.writeTypedObject(mPendingIntent, flags);
289         }
290 
291         @Override
equals(Object o)292         public boolean equals(Object o) {
293             if (this == o) return true;
294             if (!(o instanceof IconAction)) return false;
295             IconAction that = (IconAction) o;
296             return mIconType == that.mIconType && mPendingIntent.equals(that.mPendingIntent);
297         }
298 
299         @Override
hashCode()300         public int hashCode() {
301             return Objects.hash(mIconType, mPendingIntent);
302         }
303 
304         @Override
toString()305         public String toString() {
306             return "IconAction{"
307                     + "mIconType="
308                     + mIconType
309                     + ", mPendingIntent="
310                     + mPendingIntent
311                     + '}';
312         }
313 
314         @IconType
validateIconType(int value)315         private static int validateIconType(int value) {
316             switch (value) {
317                 case ICON_TYPE_GEAR:
318                 case ICON_TYPE_INFO:
319                     return value;
320                 default:
321             }
322             throw new IllegalArgumentException("Unexpected IconType for IconAction: " + value);
323         }
324     }
325 
326     /** Builder class for {@link SafetySourceStatus}. */
327     public static final class Builder {
328 
329         @NonNull private final CharSequence mTitle;
330         @NonNull private final CharSequence mSummary;
331         @SafetySourceData.SeverityLevel private final int mSeverityLevel;
332 
333         @Nullable private PendingIntent mPendingIntent;
334         @Nullable private IconAction mIconAction;
335         private boolean mEnabled = true;
336 
337         /** Creates a {@link Builder} for a {@link SafetySourceStatus}. */
Builder( @onNull CharSequence title, @NonNull CharSequence summary, @SafetySourceData.SeverityLevel int severityLevel)338         public Builder(
339                 @NonNull CharSequence title,
340                 @NonNull CharSequence summary,
341                 @SafetySourceData.SeverityLevel int severityLevel) {
342             this.mTitle = requireNonNull(title);
343             this.mSummary = requireNonNull(summary);
344             this.mSeverityLevel = validateSeverityLevel(severityLevel);
345         }
346 
347         /** Creates a {@link Builder} with the values of the given {@link SafetySourceStatus}. */
348         @RequiresApi(UPSIDE_DOWN_CAKE)
Builder(@onNull SafetySourceStatus safetySourceStatus)349         public Builder(@NonNull SafetySourceStatus safetySourceStatus) {
350             if (!SdkLevel.isAtLeastU()) {
351                 throw new UnsupportedOperationException(
352                         "Method not supported on versions lower than UPSIDE_DOWN_CAKE");
353             }
354             requireNonNull(safetySourceStatus);
355             mTitle = safetySourceStatus.mTitle;
356             mSummary = safetySourceStatus.mSummary;
357             mSeverityLevel = safetySourceStatus.mSeverityLevel;
358             mPendingIntent = safetySourceStatus.mPendingIntent;
359             mIconAction = safetySourceStatus.mIconAction;
360             mEnabled = safetySourceStatus.mEnabled;
361         }
362 
363         /**
364          * Sets an optional {@link PendingIntent} for the safety source status.
365          *
366          * <p>The action contained in the {@link PendingIntent} must start an activity.
367          *
368          * @see #getPendingIntent()
369          */
370         @NonNull
setPendingIntent(@ullable PendingIntent pendingIntent)371         public Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
372             checkArgument(
373                     pendingIntent == null || pendingIntent.isActivity(),
374                     "Safety source status pending intent must start an activity");
375             this.mPendingIntent = pendingIntent;
376             return this;
377         }
378 
379         /**
380          * Sets an optional {@link IconAction} for the safety source status.
381          *
382          * @see #getIconAction()
383          */
384         @NonNull
setIconAction(@ullable IconAction iconAction)385         public Builder setIconAction(@Nullable IconAction iconAction) {
386             this.mIconAction = iconAction;
387             return this;
388         }
389 
390         /**
391          * Sets whether the safety source status is enabled.
392          *
393          * <p>By default, the safety source status will be enabled. If disabled, the status severity
394          * level must be set to {@link SafetySourceData#SEVERITY_LEVEL_UNSPECIFIED}.
395          *
396          * @see #isEnabled()
397          */
398         @NonNull
setEnabled(boolean enabled)399         public Builder setEnabled(boolean enabled) {
400             checkArgument(
401                     enabled || mSeverityLevel == SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED,
402                     "Safety source status must have a severity level of "
403                             + "SEVERITY_LEVEL_UNSPECIFIED when disabled");
404             this.mEnabled = enabled;
405             return this;
406         }
407 
408         /** Creates the {@link SafetySourceStatus} defined by this {@link Builder}. */
409         @NonNull
build()410         public SafetySourceStatus build() {
411             return new SafetySourceStatus(
412                     mTitle, mSummary, mSeverityLevel, mPendingIntent, mIconAction, mEnabled);
413         }
414     }
415 
416     @SafetySourceData.SeverityLevel
validateSeverityLevel(int value)417     private static int validateSeverityLevel(int value) {
418         switch (value) {
419             case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED:
420             case SafetySourceData.SEVERITY_LEVEL_INFORMATION:
421             case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION:
422             case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING:
423                 return value;
424             default:
425         }
426         throw new IllegalArgumentException(
427                 "Unexpected SeverityLevel for SafetySourceStatus: " + value);
428     }
429 }
430