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