/* * 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 com.android.safetycenter; import static android.os.Build.VERSION_CODES.TIRAMISU; import static java.util.Collections.emptyList; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Binder; import android.safetycenter.SafetyCenterData; import android.safetycenter.SafetyCenterEntry; import android.safetycenter.SafetyCenterEntryGroup; import android.safetycenter.SafetyCenterEntryOrGroup; import android.safetycenter.SafetyCenterIssue; import android.safetycenter.SafetyCenterStaticEntry; import android.safetycenter.SafetyCenterStaticEntryGroup; import android.safetycenter.SafetyCenterStatus; import android.safetycenter.SafetySourceData; import android.safetycenter.SafetySourceIssue; import android.safetycenter.SafetySourceStatus; import android.safetycenter.config.SafetyCenterConfig; import android.safetycenter.config.SafetySource; import android.safetycenter.config.SafetySourcesGroup; import android.util.Log; import androidx.annotation.RequiresApi; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; /** * A class that keeps track of all the {@link SafetySourceData} set by safety sources, and * aggregates them into a {@link SafetyCenterData} object to be used by permission controller. * *

This class isn't thread safe. Thread safety must be handled by the caller. */ @RequiresApi(TIRAMISU) final class SafetyCenterDataTracker { private static final String TAG = "SafetyCenterDataTracker"; private final Map mSafetySourceDataForKey = new HashMap<>(); @NonNull private final Context mContext; @NonNull private final SafetyCenterConfigReader mSafetyCenterConfigReader; /** * Creates a {@link SafetyCenterDataTracker} using the given {@link Context} and {@link * SafetyCenterConfigReader}. */ SafetyCenterDataTracker( @NonNull Context context, @NonNull SafetyCenterConfigReader safetyCenterConfigReader) { mContext = context; mSafetyCenterConfigReader = safetyCenterConfigReader; } /** * Sets the latest {@link SafetySourceData} for the given {@code safetySourceId} and {@code * userId}, and returns the updated {@link SafetyCenterData} of the {@code userId}. * *

Returns {@code null} if there was no update to the underlying {@link SafetyCenterData}, or * if the {@link SafetyCenterConfig} is not available. */ @Nullable SafetyCenterData setSafetySourceData( @NonNull String safetySourceId, @Nullable SafetySourceData safetySourceData, @NonNull String packageName, @UserIdInt int userId) { if (!configContains(safetySourceId, packageName)) { // TODO(b/218801292): Should this be hard error for the caller? return null; } Key key = Key.of(safetySourceId, packageName, userId); SafetySourceData existingSafetySourceData = mSafetySourceDataForKey.get(key); if (Objects.equals(safetySourceData, existingSafetySourceData)) { return null; } mSafetySourceDataForKey.put(key, safetySourceData); return getSafetyCenterData(userId); } /** * Returns the latest {@link SafetySourceData} for the given {@code safetySourceId} and {@code * userId}. * *

Returns {@code null} if there was no data set. */ @Nullable SafetySourceData getSafetySourceData( @NonNull String safetySourceId, @NonNull String packageName, @UserIdInt int userId) { if (!configContains(safetySourceId, packageName)) { // TODO(b/218801292): Should this be hard error for the caller? return null; } return mSafetySourceDataForKey.get(Key.of(safetySourceId, packageName, userId)); } /** Clears all the {@link SafetySourceData} set received so far, for all users. */ void clear() { mSafetySourceDataForKey.clear(); } /** * Returns the current {@link SafetyCenterData} for the given {@code userId}, aggregated from * all the {@link SafetySourceData} set so far. * *

Returns an arbitrary default value if no data has been received for the user so far, or if * the {@link SafetyCenterConfig} is not available. */ @NonNull SafetyCenterData getSafetyCenterData(@UserIdInt int userId) { SafetyCenterConfig safetyCenterConfig = mSafetyCenterConfigReader.getSafetyCenterConfig(); if (safetyCenterConfig == null) { Log.w(TAG, "SafetyCenterConfig unavailable, returning default SafetyCenterData"); return getDefaultSafetyCenterData(); } // TODO(b/218819144): Merge for all profiles. return getSafetyCenterData(safetyCenterConfig, userId); } /** * Returns a default {@link SafetyCenterData} object to be returned when the API is disabled. */ @NonNull static SafetyCenterData getDefaultSafetyCenterData() { return new SafetyCenterData( new SafetyCenterStatus.Builder( getSafetyCenterStatusTitle( SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN), getSafetyCenterStatusSummary( SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN)) .setSeverityLevel(SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN) .build(), emptyList(), emptyList(), emptyList()); } // TODO(b/219702252): Create a more efficient data structure for this, and update it when the // config changes. private boolean configContains(@NonNull String safetySourceId, @NonNull String packageName) { SafetyCenterConfig safetyCenterConfig = mSafetyCenterConfigReader.getSafetyCenterConfig(); if (safetyCenterConfig == null) { Log.w(TAG, "SafetyCenterConfig unavailable, assuming no sources can send/get data"); return false; } // TODO(b/217944317): Remove this allowlisting once the test API for the config is // available. if (packageName.equals("android.safetycenter.cts")) { return true; } List safetySourcesGroups = safetyCenterConfig.getSafetySourcesGroups(); for (int i = 0; i < safetySourcesGroups.size(); i++) { SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i); List safetySources = safetySourcesGroup.getSafetySources(); for (int j = 0; j < safetySources.size(); j++) { SafetySource safetySource = safetySources.get(j); if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_STATIC) { continue; } if (safetySourceId.equals(safetySource.getId()) && packageName.equals(safetySource.getPackageName())) { return true; } } } return false; } @NonNull private SafetyCenterData getSafetyCenterData( @NonNull SafetyCenterConfig safetyCenterConfig, @UserIdInt int userId) { int maxSafetyCenterEntryLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN; List safetyCenterIssues = new ArrayList<>(); List safetyCenterEntryOrGroups = new ArrayList<>(); List safetyCenterStaticEntryGroups = new ArrayList<>(); List safetySourcesGroups = safetyCenterConfig.getSafetySourcesGroups(); for (int i = 0; i < safetySourcesGroups.size(); i++) { SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i); int groupSafetyCenterEntryLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN; switch (safetySourcesGroup.getType()) { case SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_COLLAPSIBLE: { groupSafetyCenterEntryLevel = Math.max( addSafetyCenterIssues( safetyCenterIssues, safetySourcesGroup, userId), addSafetyCenterEntryGroup( safetyCenterEntryOrGroups, safetySourcesGroup, userId)); break; } case SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_RIGID: { addSafetyCenterStaticEntryGroup( safetyCenterStaticEntryGroups, safetySourcesGroup); break; } case SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_HIDDEN: { groupSafetyCenterEntryLevel = addSafetyCenterIssues( safetyCenterIssues, safetySourcesGroup, userId); break; } } // TODO(b/219700241): Should we rely on ordering for severity levels? maxSafetyCenterEntryLevel = Math.max(maxSafetyCenterEntryLevel, groupSafetyCenterEntryLevel); } int safetyCenterOverallSeverityLevel = entryToSafetyCenterStatusOverallLevel(maxSafetyCenterEntryLevel); return new SafetyCenterData( new SafetyCenterStatus.Builder( getSafetyCenterStatusTitle(safetyCenterOverallSeverityLevel), getSafetyCenterStatusSummary(safetyCenterOverallSeverityLevel)) .setSeverityLevel(safetyCenterOverallSeverityLevel) .build(), safetyCenterIssues, safetyCenterEntryOrGroups, safetyCenterStaticEntryGroups); } @SafetyCenterEntry.EntrySeverityLevel private int addSafetyCenterIssues( @NonNull List safetyCenterIssues, @NonNull SafetySourcesGroup safetySourcesGroup, @UserIdInt int userId) { int maxSafetyCenterEntrySeverityLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN; List safetySources = safetySourcesGroup.getSafetySources(); for (int i = 0; i < safetySources.size(); i++) { SafetySource safetySource = safetySources.get(i); if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_STATIC) { continue; } Key key = Key.of(safetySource.getId(), safetySource.getPackageName(), userId); SafetySourceData safetySourceData = mSafetySourceDataForKey.get(key); if (safetySourceData == null) { continue; } List safetySourceIssues = safetySourceData.getIssues(); for (int j = 0; j < safetySourceIssues.size(); j++) { SafetySourceIssue safetySourceIssue = safetySourceIssues.get(j); SafetyCenterIssue safetyCenterIssue = toSafetyCenterIssue(safetySourceIssue); maxSafetyCenterEntrySeverityLevel = Math.max( maxSafetyCenterEntrySeverityLevel, issueToSafetyCenterEntryLevel( safetyCenterIssue.getSeverityLevel())); safetyCenterIssues.add(safetyCenterIssue); } } return maxSafetyCenterEntrySeverityLevel; } @NonNull private static SafetyCenterIssue toSafetyCenterIssue( @NonNull SafetySourceIssue safetySourceIssue) { List safetySourceIssueActions = safetySourceIssue.getActions(); List safetyCenterIssueActions = new ArrayList<>(safetySourceIssueActions.size()); for (int i = 0; i < safetySourceIssueActions.size(); i++) { SafetySourceIssue.Action safetySourceIssueAction = safetySourceIssueActions.get(i); safetyCenterIssueActions.add( new SafetyCenterIssue.Action.Builder( safetySourceIssue.getId(), safetySourceIssueAction.getLabel(), safetySourceIssueAction.getPendingIntent()) .setSuccessMessage(safetySourceIssueAction.getSuccessMessage()) .build()); } // TODO(b/218817233): Add missing fields like: dismissible, shouldConfirmDismissal. return new SafetyCenterIssue.Builder( safetySourceIssue.getId(), safetySourceIssue.getTitle(), safetySourceIssue.getSummary()) .setSeverityLevel( sourceToSafetyCenterIssueSeverityLevel( safetySourceIssue.getSeverityLevel())) .setSubtitle(safetySourceIssue.getSubtitle()) .setActions(safetyCenterIssueActions) .build(); } @SafetyCenterEntry.EntrySeverityLevel private int addSafetyCenterEntryGroup( @NonNull List safetyCenterEntryOrGroups, @NonNull SafetySourcesGroup safetySourcesGroup, @UserIdInt int userId) { int maxSafetyCenterEntryLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN; List safetySources = safetySourcesGroup.getSafetySources(); List entries = new ArrayList<>(safetySources.size()); for (int i = 0; i < safetySources.size(); i++) { SafetySource safetySource = safetySources.get(i); SafetyCenterEntry safetyCenterEntry = toSafetyCenterEntry(safetySource, userId); if (safetyCenterEntry == null) { continue; } // TODO(b/219700241): Should we rely on ordering for severity levels? maxSafetyCenterEntryLevel = Math.max(maxSafetyCenterEntryLevel, safetyCenterEntry.getSeverityLevel()); entries.add(safetyCenterEntry); } // TODO(b/218817233): Add missing fields like: statelessIconType. safetyCenterEntryOrGroups.add( new SafetyCenterEntryOrGroup( new SafetyCenterEntryGroup.Builder( safetySourcesGroup.getId(), mSafetyCenterConfigReader.readStringResource( safetySourcesGroup.getTitleResId())) .setSeverityLevel(maxSafetyCenterEntryLevel) .setSummary( mSafetyCenterConfigReader.readStringResource( safetySourcesGroup.getSummaryResId())) .setEntries(entries) .build())); return maxSafetyCenterEntryLevel; } @Nullable private SafetyCenterEntry toSafetyCenterEntry( @NonNull SafetySource safetySource, @UserIdInt int userId) { switch (safetySource.getType()) { case SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY: { Log.w(TAG, "Issue only safety source found in collapsible group"); return null; } case SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC: { Key key = Key.of(safetySource.getId(), safetySource.getPackageName(), userId); SafetySourceStatus safetySourceStatus = getSafetySourceStatus(mSafetySourceDataForKey.get(key)); // TODO(b/218817233): Add missing fields like: iconAction, statelessIconType. if (safetySourceStatus != null) { PendingIntent pendingIntent = safetySourceStatus.getPendingIntent(); if (pendingIntent == null) { pendingIntent = toPendingIntent( safetySource.getIntentAction(), safetySource.getPackageName()); // TODO(b/222838784): Automatically mark the source as disabled if the // pending intent is null again. } return new SafetyCenterEntry.Builder( safetySource.getId(), safetySourceStatus.getTitle()) .setSeverityLevel( sourceToSafetyCenterEntrySeverityLevel( safetySourceStatus.getSeverityLevel())) .setSummary(safetySourceStatus.getSummary()) .setEnabled(safetySourceStatus.isEnabled()) .setPendingIntent(pendingIntent) .build(); } return toDefaultSafetyCenterEntry( safetySource, safetySource.getPackageName(), SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED); } case SafetySource.SAFETY_SOURCE_TYPE_STATIC: { return toDefaultSafetyCenterEntry( safetySource, null, SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN); } } Log.w( TAG, String.format( "Unknown safety source type found in collapsible group: %s", safetySource.getType())); return null; } @Nullable private SafetyCenterEntry toDefaultSafetyCenterEntry( @NonNull SafetySource safetySource, @Nullable String packageName, @SafetyCenterEntry.EntrySeverityLevel int entrySeverityLevel) { if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC && safetySource.getInitialDisplayState() == SafetySource.INITIAL_DISPLAY_STATE_HIDDEN) { return null; } PendingIntent pendingIntent = toPendingIntent(safetySource.getIntentAction(), packageName); // TODO(b/218817233): Add missing fields like: enabled. // TODO(b/222838784): Automatically mark the source as disabled (both dynamic and static?) // if the pending intent is null. return new SafetyCenterEntry.Builder( safetySource.getId(), mSafetyCenterConfigReader.readStringResource(safetySource.getTitleResId())) .setSeverityLevel(entrySeverityLevel) .setSummary( mSafetyCenterConfigReader.readStringResource( safetySource.getSummaryResId())) .setPendingIntent(pendingIntent) .build(); } private void addSafetyCenterStaticEntryGroup( @NonNull List safetyCenterStaticEntryGroups, @NonNull SafetySourcesGroup safetySourcesGroup) { List safetySources = safetySourcesGroup.getSafetySources(); List staticEntries = new ArrayList<>(safetySources.size()); for (int i = 0; i < safetySources.size(); i++) { SafetySource safetySource = safetySources.get(i); if (safetySource.getType() != SafetySource.SAFETY_SOURCE_TYPE_STATIC) { Log.w(TAG, "Non-static safety source found in rigid group"); continue; } PendingIntent pendingIntent = toPendingIntent(safetySource.getIntentAction(), null); if (pendingIntent == null) { // TODO(b/222838784): Decide strategy for static entries when the intent is null. continue; } staticEntries.add( new SafetyCenterStaticEntry.Builder( mSafetyCenterConfigReader.readStringResource( safetySource.getTitleResId())) .setSummary( mSafetyCenterConfigReader.readStringResource( safetySource.getSummaryResId())) .setPendingIntent(pendingIntent) .build()); } safetyCenterStaticEntryGroups.add( new SafetyCenterStaticEntryGroup( mSafetyCenterConfigReader.readStringResource( safetySourcesGroup.getTitleResId()), staticEntries)); } @Nullable private PendingIntent toPendingIntent( @Nullable String intentAction, @Nullable String packageName) { if (intentAction == null) { return null; } Context context; if (packageName == null) { context = mContext; } else { final long identity = Binder.clearCallingIdentity(); try { context = mContext.createPackageContext(packageName, 0); } catch (NameNotFoundException e) { Log.w(TAG, String.format("Package name %s not found", packageName), e); return null; } finally { Binder.restoreCallingIdentity(identity); } } // TODO(b/222838784): Validate that the intent action is available. // TODO(b/219699223): Is it safe to create a PendingIntent as system server here? final long identity = Binder.clearCallingIdentity(); try { // TODO(b/218816518): May need to create a unique requestCode per PendingIntent. return PendingIntent.getActivity( context, 0, new Intent(intentAction), PendingIntent.FLAG_IMMUTABLE); } finally { Binder.restoreCallingIdentity(identity); } } @Nullable private static SafetySourceStatus getSafetySourceStatus( @Nullable SafetySourceData safetySourceData) { if (safetySourceData == null) { return null; } return safetySourceData.getStatus(); } @SafetyCenterStatus.OverallSeverityLevel private static int entryToSafetyCenterStatusOverallLevel( @SafetyCenterEntry.EntrySeverityLevel int safetyCenterEntrySeverityLevel) { switch (safetyCenterEntrySeverityLevel) { case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN: return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN; case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED: case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK: return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK; case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION: return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION; case SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING: return SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING; } throw new IllegalArgumentException( String.format( "Unexpected SafetyCenterEntry.EntrySeverityLevel: %s", safetyCenterEntrySeverityLevel)); } @SafetyCenterEntry.EntrySeverityLevel private static int issueToSafetyCenterEntryLevel( @SafetyCenterIssue.IssueSeverityLevel int safetyCenterIssueSeverityLevel) { switch (safetyCenterIssueSeverityLevel) { case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK: return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK; case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION: return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION; case SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING: return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING; } throw new IllegalArgumentException( String.format( "Unexpected SafetyCenterIssue.IssueSeverityLevel: %s", safetyCenterIssueSeverityLevel)); } @SafetyCenterEntry.EntrySeverityLevel private static int sourceToSafetyCenterEntrySeverityLevel( @SafetySourceData.SeverityLevel int safetySourceSeverityLevel) { switch (safetySourceSeverityLevel) { case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED; case SafetySourceData.SEVERITY_LEVEL_INFORMATION: return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK; case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION; case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING; } throw new IllegalArgumentException( String.format( "Unexpected SafetySourceSeverity.Level in SafetySourceStatus: %s", safetySourceSeverityLevel)); } @SafetyCenterIssue.IssueSeverityLevel private static int sourceToSafetyCenterIssueSeverityLevel( @SafetySourceData.SeverityLevel int safetySourceIssueSeverityLevel) { switch (safetySourceIssueSeverityLevel) { case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED: Log.w( TAG, "Unexpected use of SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED in " + "SafetySourceStatus"); return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK; case SafetySourceData.SEVERITY_LEVEL_INFORMATION: return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK; case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION: return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION; case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING: return SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING; } throw new IllegalArgumentException( String.format( "Unexpected SafetySourceSeverity.Level in SafetySourceIssue: %s", safetySourceIssueSeverityLevel)); } // TODO(b/218801295): Use the right strings and localize them. private static String getSafetyCenterStatusTitle( @SafetyCenterStatus.OverallSeverityLevel int overallSeverityLevel) { switch (overallSeverityLevel) { case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN: return "Unknown"; case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK: return "All good"; case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION: return "Some warnings"; case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING: return "Uh-oh"; } throw new IllegalArgumentException( String.format( "Unexpected SafetyCenterStatus.OverallSeverityLevel: %s", overallSeverityLevel)); } // TODO(b/218801295): Use the right strings and localize them. private static String getSafetyCenterStatusSummary( @SafetyCenterStatus.OverallSeverityLevel int overallSeverityLevel) { switch (overallSeverityLevel) { case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN: return "Unknown safety status"; case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK: return "No problemo maestro"; case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION: return "Careful there"; case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING: return "Code red"; } throw new IllegalArgumentException( String.format( "Unexpected SafetyCenterStatus.OverallSeverityLevel: %s", overallSeverityLevel)); } /** * A key for {@link SafetySourceData}; based on the {@code safetySourceId}, {@code packageName} * and {@code userId}. */ // TODO(b/219697341): Look into using AutoValue for this data class. private static final class Key { @NonNull private final String mSafetySourceId; @NonNull private final String mPackageName; @UserIdInt private final int mUserId; private Key( @NonNull String safetySourceId, @NonNull String packageName, @UserIdInt int userId) { mSafetySourceId = safetySourceId; mPackageName = packageName; mUserId = userId; } @NonNull private static Key of( @NonNull String safetySourceId, @NonNull String packageName, @UserIdInt int userId) { return new Key(safetySourceId, packageName, userId); } @Override public String toString() { return "Key{" + "mSafetySourceId='" + mSafetySourceId + '\'' + ", mPackageName='" + mPackageName + '\'' + ", mUserId=" + mUserId + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Key)) return false; Key key = (Key) o; return mSafetySourceId.equals(key.mSafetySourceId) && mPackageName.equals(key.mPackageName) && mUserId == key.mUserId; } @Override public int hashCode() { return Objects.hash(mSafetySourceId, mPackageName, mUserId); } } }