/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.safetycenter; import static android.os.Build.VERSION_CODES.TIRAMISU; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.RequiresApi; import com.android.modules.utils.build.SdkLevel; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; /** * A representation of the safety state of the device. * * @hide */ @SystemApi @RequiresApi(TIRAMISU) public final class SafetyCenterData implements Parcelable { /** * A key used in {@link #getExtras()} to map {@link SafetyCenterIssue} ids to their associated * {@link SafetyCenterEntryGroup} ids. */ private static final String ISSUES_TO_GROUPS_BUNDLE_KEY = "IssuesToGroups"; /** * A key used in {@link #getExtras()} to map {@link SafetyCenterStaticEntry} to their associated * ids. * *

{@link SafetyCenterStaticEntry} are keyed by {@code * SafetyCenterIds.toBundleKey(safetyCenterStaticEntry)}. */ private static final String STATIC_ENTRIES_TO_IDS_BUNDLE_KEY = "StaticEntriesToIds"; @NonNull public static final Creator CREATOR = new Creator() { @Override public SafetyCenterData createFromParcel(Parcel in) { SafetyCenterStatus status = in.readTypedObject(SafetyCenterStatus.CREATOR); List issues = in.createTypedArrayList(SafetyCenterIssue.CREATOR); List entryOrGroups = in.createTypedArrayList(SafetyCenterEntryOrGroup.CREATOR); List staticEntryGroups = in.createTypedArrayList(SafetyCenterStaticEntryGroup.CREATOR); if (SdkLevel.isAtLeastU()) { List dismissedIssues = in.createTypedArrayList(SafetyCenterIssue.CREATOR); Bundle extras = in.readBundle(getClass().getClassLoader()); SafetyCenterData.Builder builder = new SafetyCenterData.Builder(status); for (int i = 0; i < issues.size(); i++) { builder.addIssue(issues.get(i)); } for (int i = 0; i < entryOrGroups.size(); i++) { builder.addEntryOrGroup(entryOrGroups.get(i)); } for (int i = 0; i < staticEntryGroups.size(); i++) { builder.addStaticEntryGroup(staticEntryGroups.get(i)); } for (int i = 0; i < dismissedIssues.size(); i++) { builder.addDismissedIssue(dismissedIssues.get(i)); } if (extras != null) { builder.setExtras(extras); } return builder.build(); } else { return new SafetyCenterData( status, issues, entryOrGroups, staticEntryGroups); } } @Override public SafetyCenterData[] newArray(int size) { return new SafetyCenterData[size]; } }; @NonNull private final SafetyCenterStatus mStatus; @NonNull private final List mIssues; @NonNull private final List mEntriesOrGroups; @NonNull private final List mStaticEntryGroups; @NonNull private final List mDismissedIssues; @NonNull private final Bundle mExtras; /** Creates a {@link SafetyCenterData}. */ public SafetyCenterData( @NonNull SafetyCenterStatus status, @NonNull List issues, @NonNull List entriesOrGroups, @NonNull List staticEntryGroups) { mStatus = requireNonNull(status); mIssues = unmodifiableList(new ArrayList<>(requireNonNull(issues))); mEntriesOrGroups = unmodifiableList(new ArrayList<>(requireNonNull(entriesOrGroups))); mStaticEntryGroups = unmodifiableList(new ArrayList<>(requireNonNull(staticEntryGroups))); mDismissedIssues = unmodifiableList(new ArrayList<>()); mExtras = Bundle.EMPTY; } private SafetyCenterData( @NonNull SafetyCenterStatus status, @NonNull List issues, @NonNull List entriesOrGroups, @NonNull List staticEntryGroups, @NonNull List dismissedIssues, @NonNull Bundle extras) { mStatus = status; mIssues = issues; mEntriesOrGroups = entriesOrGroups; mStaticEntryGroups = staticEntryGroups; mDismissedIssues = dismissedIssues; mExtras = extras; } /** Returns the overall {@link SafetyCenterStatus} of the Safety Center. */ @NonNull public SafetyCenterStatus getStatus() { return mStatus; } /** Returns the list of active {@link SafetyCenterIssue} objects in the Safety Center. */ @NonNull public List getIssues() { return mIssues; } /** * Returns the structured list of {@link SafetyCenterEntry} and {@link SafetyCenterEntryGroup} * objects, wrapped in {@link SafetyCenterEntryOrGroup}. */ @NonNull public List getEntriesOrGroups() { return mEntriesOrGroups; } /** Returns the list of {@link SafetyCenterStaticEntryGroup} objects in the Safety Center. */ @NonNull public List getStaticEntryGroups() { return mStaticEntryGroups; } /** Returns the list of dismissed {@link SafetyCenterIssue} objects in the Safety Center. */ @NonNull @RequiresApi(UPSIDE_DOWN_CAKE) public List getDismissedIssues() { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException( "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); } return mDismissedIssues; } /** * Returns a {@link Bundle} containing additional information, {@link Bundle#EMPTY} by default. * *

Note: internal state of this {@link Bundle} is not used for {@link Object#equals} and * {@link Object#hashCode} implementation of {@link SafetyCenterData}. */ @NonNull @RequiresApi(UPSIDE_DOWN_CAKE) public Bundle getExtras() { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException( "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); } return mExtras; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof SafetyCenterData)) return false; SafetyCenterData that = (SafetyCenterData) o; return Objects.equals(mStatus, that.mStatus) && Objects.equals(mIssues, that.mIssues) && Objects.equals(mEntriesOrGroups, that.mEntriesOrGroups) && Objects.equals(mStaticEntryGroups, that.mStaticEntryGroups) && Objects.equals(mDismissedIssues, that.mDismissedIssues) && areKnownExtrasContentsEqual(mExtras, that.mExtras); } /** We're only comparing the bundle data that we know of. */ private static boolean areKnownExtrasContentsEqual( @NonNull Bundle left, @NonNull Bundle right) { return areBundlesEqual(left, right, ISSUES_TO_GROUPS_BUNDLE_KEY) && areBundlesEqual(left, right, STATIC_ENTRIES_TO_IDS_BUNDLE_KEY); } private static boolean areBundlesEqual( @NonNull Bundle left, @NonNull Bundle right, @NonNull String bundleKey) { Bundle leftBundle = left.getBundle(bundleKey); Bundle rightBundle = right.getBundle(bundleKey); if (leftBundle == null && rightBundle == null) { return true; } if (leftBundle == null || rightBundle == null) { return false; } Set leftKeys = leftBundle.keySet(); Set rightKeys = rightBundle.keySet(); if (!Objects.equals(leftKeys, rightKeys)) { return false; } for (String key : leftKeys) { if (!Objects.equals( getBundleValue(leftBundle, bundleKey, key), getBundleValue(rightBundle, bundleKey, key))) { return false; } } return true; } @Override public int hashCode() { return Objects.hash( mStatus, mIssues, mEntriesOrGroups, mStaticEntryGroups, mDismissedIssues, getExtrasHash()); } /** We're only hashing bundle data that we know of. */ private int getExtrasHash() { return Objects.hash( bundleHash(ISSUES_TO_GROUPS_BUNDLE_KEY), bundleHash(STATIC_ENTRIES_TO_IDS_BUNDLE_KEY)); } private int bundleHash(@NonNull String bundleKey) { Bundle bundle = mExtras.getBundle(bundleKey); if (bundle == null) { return 0; } int hash = 0; for (String key : bundle.keySet()) { hash += Objects.hashCode(key) ^ Objects.hashCode(getBundleValue(bundle, bundleKey, key)); } return hash; } @Override public String toString() { return "SafetyCenterData{" + "mStatus=" + mStatus + ", mIssues=" + mIssues + ", mEntriesOrGroups=" + mEntriesOrGroups + ", mStaticEntryGroups=" + mStaticEntryGroups + ", mDismissedIssues=" + mDismissedIssues + ", mExtras=" + extrasToString() + '}'; } /** We're only including bundle data that we know of. */ @NonNull private String extrasToString() { int knownExtras = 0; StringBuilder sb = new StringBuilder(); if (appendBundleString(sb, ISSUES_TO_GROUPS_BUNDLE_KEY)) { knownExtras++; } if (appendBundleString(sb, STATIC_ENTRIES_TO_IDS_BUNDLE_KEY)) { knownExtras++; } boolean hasUnknownExtras = knownExtras != mExtras.keySet().size(); if (hasUnknownExtras) { sb.append("(has unknown extras)"); } else if (knownExtras == 0) { sb.append("(no extras)"); } return sb.toString(); } private boolean appendBundleString(@NonNull StringBuilder sb, @NonNull String bundleKey) { Bundle bundle = mExtras.getBundle(bundleKey); if (bundle == null) { return false; } sb.append(bundleKey); sb.append(":["); for (String key : bundle.keySet()) { sb.append("(key=") .append(key) .append(";value=") .append(getBundleValue(bundle, bundleKey, key)) .append(")"); } sb.append("]"); return true; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mStatus, flags); dest.writeTypedList(mIssues); dest.writeTypedList(mEntriesOrGroups); dest.writeTypedList(mStaticEntryGroups); if (SdkLevel.isAtLeastU()) { dest.writeTypedList(mDismissedIssues); dest.writeBundle(mExtras); } } /** Builder class for {@link SafetyCenterData}. */ @RequiresApi(UPSIDE_DOWN_CAKE) public static final class Builder { @NonNull private final SafetyCenterStatus mStatus; @NonNull private final List mIssues = new ArrayList<>(); @NonNull private final List mEntriesOrGroups = new ArrayList<>(); @NonNull private final List mStaticEntryGroups = new ArrayList<>(); @NonNull private final List mDismissedIssues = new ArrayList<>(); @NonNull private Bundle mExtras = Bundle.EMPTY; public Builder(@NonNull SafetyCenterStatus status) { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException( "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); } mStatus = requireNonNull(status); } /** Creates a {@link Builder} with the values from the given {@link SafetyCenterData}. */ public Builder(@NonNull SafetyCenterData safetyCenterData) { if (!SdkLevel.isAtLeastU()) { throw new UnsupportedOperationException( "Method not supported on versions lower than UPSIDE_DOWN_CAKE"); } requireNonNull(safetyCenterData); mStatus = safetyCenterData.mStatus; mIssues.addAll(safetyCenterData.mIssues); mEntriesOrGroups.addAll(safetyCenterData.mEntriesOrGroups); mStaticEntryGroups.addAll(safetyCenterData.mStaticEntryGroups); mDismissedIssues.addAll(safetyCenterData.mDismissedIssues); mExtras = safetyCenterData.mExtras.deepCopy(); } /** Adds data for a {@link SafetyCenterIssue} to be shown in UI. */ @NonNull public SafetyCenterData.Builder addIssue(@NonNull SafetyCenterIssue safetyCenterIssue) { mIssues.add(requireNonNull(safetyCenterIssue)); return this; } /** Adds data for a {@link SafetyCenterEntryOrGroup} to be shown in UI. */ @NonNull @SuppressWarnings("MissingGetterMatchingBuilder") // incorrectly expects "getEntryOrGroups" public SafetyCenterData.Builder addEntryOrGroup( @NonNull SafetyCenterEntryOrGroup safetyCenterEntryOrGroup) { mEntriesOrGroups.add(requireNonNull(safetyCenterEntryOrGroup)); return this; } /** Adds data for a {@link SafetyCenterStaticEntryGroup} to be shown in UI. */ @NonNull public SafetyCenterData.Builder addStaticEntryGroup( @NonNull SafetyCenterStaticEntryGroup safetyCenterStaticEntryGroup) { mStaticEntryGroups.add(requireNonNull(safetyCenterStaticEntryGroup)); return this; } /** Adds data for a dismissed {@link SafetyCenterIssue} to be shown in UI. */ @NonNull public SafetyCenterData.Builder addDismissedIssue( @NonNull SafetyCenterIssue dismissedSafetyCenterIssue) { mDismissedIssues.add(requireNonNull(dismissedSafetyCenterIssue)); return this; } /** * Sets additional information for the {@link SafetyCenterData}. * *

If not set, the default value is {@link Bundle#EMPTY}. */ @NonNull public SafetyCenterData.Builder setExtras(@NonNull Bundle extras) { mExtras = requireNonNull(extras); return this; } /** * Resets additional information for the {@link SafetyCenterData} to the default value of * {@link Bundle#EMPTY}. */ @NonNull public SafetyCenterData.Builder clearExtras() { mExtras = Bundle.EMPTY; return this; } /** * Clears data for all the {@link SafetyCenterIssue}s that were added to this {@link * SafetyCenterData.Builder}. */ @NonNull public SafetyCenterData.Builder clearIssues() { mIssues.clear(); return this; } /** * Clears data for all the {@link SafetyCenterEntryOrGroup}s that were added to this {@link * SafetyCenterData.Builder}. */ @NonNull public SafetyCenterData.Builder clearEntriesOrGroups() { mEntriesOrGroups.clear(); return this; } /** * Clears data for all the {@link SafetyCenterStaticEntryGroup}s that were added to this * {@link SafetyCenterData.Builder}. */ @NonNull public SafetyCenterData.Builder clearStaticEntryGroups() { mStaticEntryGroups.clear(); return this; } /** * Clears data for all the dismissed {@link SafetyCenterIssue}s that were added to this * {@link SafetyCenterData.Builder}. */ @NonNull public SafetyCenterData.Builder clearDismissedIssues() { mDismissedIssues.clear(); return this; } /** * Creates the {@link SafetyCenterData} defined by this {@link SafetyCenterData.Builder}. */ @NonNull public SafetyCenterData build() { List issues = unmodifiableList(new ArrayList<>(mIssues)); List entriesOrGroups = unmodifiableList(new ArrayList<>(mEntriesOrGroups)); List staticEntryGroups = unmodifiableList(new ArrayList<>(mStaticEntryGroups)); List dismissedIssues = unmodifiableList(new ArrayList<>(mDismissedIssues)); return new SafetyCenterData( mStatus, issues, entriesOrGroups, staticEntryGroups, dismissedIssues, mExtras); } } @Nullable private static Object getBundleValue( @NonNull Bundle bundle, @NonNull String bundleParentKey, @NonNull String key) { switch (bundleParentKey) { case ISSUES_TO_GROUPS_BUNDLE_KEY: return bundle.getStringArrayList(key); case STATIC_ENTRIES_TO_IDS_BUNDLE_KEY: return bundle.getString(key); default: } throw new IllegalArgumentException("Unexpected bundle parent key: " + bundleParentKey); } }