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