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