| /* |
| * Copyright (C) 2023 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.data; |
| |
| import static android.os.Build.VERSION_CODES.TIRAMISU; |
| |
| import static com.android.safetycenter.logging.SafetyCenterStatsdLogger.toSystemEventResult; |
| |
| import android.annotation.Nullable; |
| import android.annotation.UserIdInt; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.pm.Signature; |
| import android.safetycenter.SafetyCenterData; |
| import android.safetycenter.SafetyEvent; |
| import android.safetycenter.SafetySourceData; |
| import android.safetycenter.SafetySourceErrorDetails; |
| import android.safetycenter.SafetySourceIssue; |
| import android.safetycenter.SafetySourceStatus; |
| import android.safetycenter.config.SafetyCenterConfig; |
| import android.safetycenter.config.SafetySource; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| |
| import androidx.annotation.RequiresApi; |
| |
| import com.android.modules.utils.build.SdkLevel; |
| import com.android.permission.util.UserUtils; |
| import com.android.safetycenter.SafetyCenterConfigReader; |
| import com.android.safetycenter.SafetyCenterFlags; |
| import com.android.safetycenter.SafetyCenterRefreshTracker; |
| import com.android.safetycenter.SafetySourceKey; |
| import com.android.safetycenter.SafetySources; |
| import com.android.safetycenter.UserProfileGroup; |
| import com.android.safetycenter.internaldata.SafetyCenterIssueActionId; |
| import com.android.safetycenter.internaldata.SafetyCenterIssueKey; |
| |
| import java.io.PrintWriter; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| import javax.annotation.concurrent.NotThreadSafe; |
| |
| /** |
| * Repository for {@link SafetySourceData} and other data managed by Safety Center including {@link |
| * SafetySourceErrorDetails}. |
| * |
| * <p>This class isn't thread safe. Thread safety must be handled by the caller. |
| */ |
| @RequiresApi(TIRAMISU) |
| @NotThreadSafe |
| final class SafetySourceDataRepository { |
| |
| private static final String TAG = "SafetySourceDataRepo"; |
| |
| private final ArrayMap<SafetySourceKey, SafetySourceData> mSafetySourceDataForKey = |
| new ArrayMap<>(); |
| |
| private final ArraySet<SafetySourceKey> mSafetySourceErrors = new ArraySet<>(); |
| |
| private final Context mContext; |
| private final SafetyCenterConfigReader mSafetyCenterConfigReader; |
| private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker; |
| private final SafetyCenterInFlightIssueActionRepository |
| mSafetyCenterInFlightIssueActionRepository; |
| private final SafetyCenterIssueDismissalRepository mSafetyCenterIssueDismissalRepository; |
| private final PackageManager mPackageManager; |
| |
| SafetySourceDataRepository( |
| Context context, |
| SafetyCenterConfigReader safetyCenterConfigReader, |
| SafetyCenterRefreshTracker safetyCenterRefreshTracker, |
| SafetyCenterInFlightIssueActionRepository safetyCenterInFlightIssueActionRepository, |
| SafetyCenterIssueDismissalRepository safetyCenterIssueDismissalRepository) { |
| mContext = context; |
| mSafetyCenterConfigReader = safetyCenterConfigReader; |
| mSafetyCenterRefreshTracker = safetyCenterRefreshTracker; |
| mSafetyCenterInFlightIssueActionRepository = safetyCenterInFlightIssueActionRepository; |
| mSafetyCenterIssueDismissalRepository = safetyCenterIssueDismissalRepository; |
| mPackageManager = mContext.getPackageManager(); |
| } |
| |
| /** |
| * Sets the latest {@link SafetySourceData} for the given {@code safetySourceId}, {@link |
| * SafetyEvent}, {@code packageName} and {@code userId}, and returns whether there was a change |
| * to the underlying {@link SafetyCenterData}. |
| * |
| * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code |
| * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected; or the {@link |
| * SafetySourceData} does not respect all constraints defined in the config. |
| * |
| * <p>Setting a {@code null} {@link SafetySourceData} evicts the current {@link |
| * SafetySourceData} entry and clears the {@link SafetyCenterIssueDismissalRepository} for the |
| * source. |
| * |
| * <p>This method may modify the {@link SafetyCenterIssueDismissalRepository}. |
| */ |
| boolean setSafetySourceData( |
| @Nullable SafetySourceData safetySourceData, |
| String safetySourceId, |
| SafetyEvent safetyEvent, |
| String packageName, |
| @UserIdInt int userId) { |
| if (!validateRequest(safetySourceData, safetySourceId, packageName, userId)) { |
| return false; |
| } |
| boolean safetyEventChangedSafetyCenterData = |
| processSafetyEvent(safetySourceId, safetyEvent, userId, false); |
| |
| SafetySourceKey key = SafetySourceKey.of(safetySourceId, userId); |
| boolean removingSafetySourceErrorChangedSafetyCenterData = mSafetySourceErrors.remove(key); |
| SafetySourceData existingSafetySourceData = mSafetySourceDataForKey.get(key); |
| SafetySourceData fixedSafetySourceData = |
| AndroidLockScreenFix.maybeOverrideSafetySourceData( |
| mContext, safetySourceId, safetySourceData); |
| if (Objects.equals(fixedSafetySourceData, existingSafetySourceData)) { |
| return safetyEventChangedSafetyCenterData |
| || removingSafetySourceErrorChangedSafetyCenterData; |
| } |
| |
| ArraySet<String> issueIds = new ArraySet<>(); |
| if (fixedSafetySourceData == null) { |
| mSafetySourceDataForKey.remove(key); |
| } else { |
| mSafetySourceDataForKey.put(key, fixedSafetySourceData); |
| for (int i = 0; i < fixedSafetySourceData.getIssues().size(); i++) { |
| issueIds.add(fixedSafetySourceData.getIssues().get(i).getId()); |
| } |
| } |
| mSafetyCenterIssueDismissalRepository.updateIssuesForSource( |
| issueIds, safetySourceId, userId); |
| |
| return true; |
| } |
| |
| /** |
| * Returns the latest {@link SafetySourceData} that was set by {@link #setSafetySourceData} for |
| * the given {@code safetySourceId}, {@code packageName} and {@code userId}. |
| * |
| * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code |
| * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected. |
| * |
| * <p>Returns {@code null} if it was never set since boot, or if the entry was evicted using |
| * {@link #setSafetySourceData} with a {@code null} value. |
| */ |
| @Nullable |
| SafetySourceData getSafetySourceData( |
| String safetySourceId, String packageName, @UserIdInt int userId) { |
| if (!validateRequest(null, safetySourceId, packageName, userId)) { |
| return null; |
| } |
| return getSafetySourceDataInternal(SafetySourceKey.of(safetySourceId, userId)); |
| } |
| |
| /** |
| * Returns the latest {@link SafetySourceData} that was set by {@link #setSafetySourceData} for |
| * the given {@link SafetySourceKey}. |
| * |
| * <p>This method does not perform any validation, {@link #getSafetySourceData(String, String, |
| * int)} should be called wherever validation is required. |
| * |
| * <p>Returns {@code null} if it was never set since boot, or if the entry was evicted using |
| * {@link #setSafetySourceData} with a {@code null} value. |
| */ |
| @Nullable |
| SafetySourceData getSafetySourceDataInternal(SafetySourceKey safetySourceKey) { |
| return mSafetySourceDataForKey.get(safetySourceKey); |
| } |
| |
| /** |
| * Reports the given {@link SafetySourceErrorDetails} for the given {@code safetySourceId} and |
| * {@code userId}, and returns whether there was a change to the underlying {@link |
| * SafetyCenterData}. |
| * |
| * <p>Throws if the request is invalid based on the {@link SafetyCenterConfig}: the given {@code |
| * safetySourceId}, {@code packageName} and/or {@code userId} are unexpected. |
| */ |
| boolean reportSafetySourceError( |
| SafetySourceErrorDetails safetySourceErrorDetails, |
| String safetySourceId, |
| String packageName, |
| @UserIdInt int userId) { |
| if (!validateRequest(null, safetySourceId, packageName, userId)) { |
| return false; |
| } |
| SafetyEvent safetyEvent = safetySourceErrorDetails.getSafetyEvent(); |
| Log.w(TAG, "Error reported from source: " + safetySourceId + ", for event: " + safetyEvent); |
| |
| boolean safetyEventChangedSafetyCenterData = |
| processSafetyEvent(safetySourceId, safetyEvent, userId, true); |
| int safetyEventType = safetyEvent.getType(); |
| if (safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED |
| || safetyEventType == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) { |
| return safetyEventChangedSafetyCenterData; |
| } |
| |
| SafetySourceKey key = SafetySourceKey.of(safetySourceId, userId); |
| boolean safetySourceErrorChangedSafetyCenterData = setSafetySourceError(key); |
| return safetyEventChangedSafetyCenterData || safetySourceErrorChangedSafetyCenterData; |
| } |
| |
| /** |
| * Marks the given {@link SafetySourceKey} as having errored-out and returns whether there was a |
| * change to the underlying {@link SafetyCenterData}. |
| */ |
| boolean setSafetySourceError(SafetySourceKey safetySourceKey) { |
| boolean removingSafetySourceDataChangedSafetyCenterData = |
| mSafetySourceDataForKey.remove(safetySourceKey) != null; |
| boolean addingSafetySourceErrorChangedSafetyCenterData = |
| mSafetySourceErrors.add(safetySourceKey); |
| return removingSafetySourceDataChangedSafetyCenterData |
| || addingSafetySourceErrorChangedSafetyCenterData; |
| } |
| |
| /** |
| * Clears all safety source errors received so far for the given {@link UserProfileGroup}, this |
| * is useful e.g. when starting a new broadcast. |
| */ |
| void clearSafetySourceErrors(UserProfileGroup userProfileGroup) { |
| // Loop in reverse index order to be able to remove entries while iterating. |
| for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) { |
| SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i); |
| if (userProfileGroup.contains(sourceKey.getUserId())) { |
| mSafetySourceErrors.removeAt(i); |
| } |
| } |
| } |
| |
| /** |
| * Returns the {@link SafetySourceIssue} associated with the given {@link SafetyCenterIssueKey}. |
| * |
| * <p>Returns {@code null} if there is no such {@link SafetySourceIssue}. |
| */ |
| @Nullable |
| SafetySourceIssue getSafetySourceIssue(SafetyCenterIssueKey safetyCenterIssueKey) { |
| SafetySourceKey key = |
| SafetySourceKey.of( |
| safetyCenterIssueKey.getSafetySourceId(), safetyCenterIssueKey.getUserId()); |
| SafetySourceData safetySourceData = mSafetySourceDataForKey.get(key); |
| if (safetySourceData == null) { |
| return null; |
| } |
| List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues(); |
| |
| SafetySourceIssue targetIssue = null; |
| for (int i = 0; i < safetySourceIssues.size(); i++) { |
| SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i); |
| |
| if (safetyCenterIssueKey.getSafetySourceIssueId().equals(safetySourceIssue.getId())) { |
| targetIssue = safetySourceIssue; |
| break; |
| } |
| } |
| |
| return targetIssue; |
| } |
| |
| /** |
| * Returns the {@link SafetySourceIssue.Action} associated with the given {@link |
| * SafetyCenterIssueActionId}. |
| * |
| * <p>Returns {@code null} if there is no associated {@link SafetySourceIssue}. |
| * |
| * <p>Returns {@code null} if the {@link SafetySourceIssue.Action} is currently in flight. |
| */ |
| @Nullable |
| SafetySourceIssue.Action getSafetySourceIssueAction( |
| SafetyCenterIssueActionId safetyCenterIssueActionId) { |
| SafetySourceIssue safetySourceIssue = |
| getSafetySourceIssue(safetyCenterIssueActionId.getSafetyCenterIssueKey()); |
| |
| if (safetySourceIssue == null) { |
| return null; |
| } |
| |
| return mSafetyCenterInFlightIssueActionRepository.getSafetySourceIssueAction( |
| safetyCenterIssueActionId, safetySourceIssue); |
| } |
| |
| /** Clears all {@link SafetySourceData}, errors, issues and in flight actions for all users. */ |
| void clear() { |
| mSafetySourceDataForKey.clear(); |
| mSafetySourceErrors.clear(); |
| } |
| |
| /** |
| * Clears all {@link SafetySourceData}, errors, issues and in flight actions, for the given |
| * user. |
| */ |
| void clearForUser(@UserIdInt int userId) { |
| // Loop in reverse index order to be able to remove entries while iterating. |
| for (int i = mSafetySourceDataForKey.size() - 1; i >= 0; i--) { |
| SafetySourceKey sourceKey = mSafetySourceDataForKey.keyAt(i); |
| if (sourceKey.getUserId() == userId) { |
| mSafetySourceDataForKey.removeAt(i); |
| } |
| } |
| // Loop in reverse index order to be able to remove entries while iterating. |
| for (int i = mSafetySourceErrors.size() - 1; i >= 0; i--) { |
| SafetySourceKey sourceKey = mSafetySourceErrors.valueAt(i); |
| if (sourceKey.getUserId() == userId) { |
| mSafetySourceErrors.removeAt(i); |
| } |
| } |
| } |
| |
| /** Dumps state for debugging purposes. */ |
| void dump(PrintWriter fout) { |
| int dataCount = mSafetySourceDataForKey.size(); |
| fout.println("SOURCE DATA (" + dataCount + ")"); |
| for (int i = 0; i < dataCount; i++) { |
| SafetySourceKey key = mSafetySourceDataForKey.keyAt(i); |
| SafetySourceData data = mSafetySourceDataForKey.valueAt(i); |
| fout.println("\t[" + i + "] " + key + " -> " + data); |
| } |
| fout.println(); |
| |
| int errorCount = mSafetySourceErrors.size(); |
| fout.println("SOURCE ERRORS (" + errorCount + ")"); |
| for (int i = 0; i < errorCount; i++) { |
| SafetySourceKey key = mSafetySourceErrors.valueAt(i); |
| fout.println("\t[" + i + "] " + key); |
| } |
| fout.println(); |
| } |
| |
| /** Returns {@code true} if the given source has an error. */ |
| boolean sourceHasError(SafetySourceKey safetySourceKey) { |
| return mSafetySourceErrors.contains(safetySourceKey); |
| } |
| |
| /** |
| * Checks if a request to the SafetyCenter is valid, and returns whether the request should be |
| * processed. |
| */ |
| private boolean validateRequest( |
| @Nullable SafetySourceData safetySourceData, |
| String safetySourceId, |
| String packageName, |
| @UserIdInt int userId) { |
| SafetyCenterConfigReader.ExternalSafetySource externalSafetySource = |
| mSafetyCenterConfigReader.getExternalSafetySource(safetySourceId, packageName); |
| if (externalSafetySource == null) { |
| throw new IllegalArgumentException("Unexpected safety source: " + safetySourceId); |
| } |
| |
| SafetySource safetySource = externalSafetySource.getSafetySource(); |
| validateCallingPackage(safetySource, packageName, safetySourceId); |
| |
| if (UserUtils.isManagedProfile(userId, mContext) |
| && !SafetySources.supportsManagedProfiles(safetySource)) { |
| throw new IllegalArgumentException( |
| "Unexpected managed profile request for safety source: " + safetySourceId); |
| } |
| |
| boolean retrievingOrClearingData = safetySourceData == null; |
| if (retrievingOrClearingData) { |
| return mSafetyCenterConfigReader.isExternalSafetySourceActive( |
| safetySourceId, packageName); |
| } |
| |
| SafetySourceStatus safetySourceStatus = safetySourceData.getStatus(); |
| |
| if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY |
| && safetySourceStatus != null) { |
| throw new IllegalArgumentException( |
| "Unexpected status for issue only safety source: " + safetySourceId); |
| } |
| |
| if (safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC |
| && safetySourceStatus == null) { |
| throw new IllegalArgumentException( |
| "Missing status for dynamic safety source: " + safetySourceId); |
| } |
| |
| if (safetySourceStatus != null) { |
| int sourceSeverityLevel = safetySourceStatus.getSeverityLevel(); |
| |
| if (externalSafetySource.hasEntryInStatelessGroup() |
| && sourceSeverityLevel != SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED) { |
| throw new IllegalArgumentException( |
| "Safety source: " |
| + safetySourceId |
| + " is in a stateless group but specified a severity level: " |
| + sourceSeverityLevel); |
| } |
| |
| int maxSourceSeverityLevel = |
| Math.max( |
| SafetySourceData.SEVERITY_LEVEL_INFORMATION, |
| safetySource.getMaxSeverityLevel()); |
| |
| if (sourceSeverityLevel > maxSourceSeverityLevel) { |
| throw new IllegalArgumentException( |
| "Unexpected severity level: " |
| + sourceSeverityLevel |
| + ", for safety source: " |
| + safetySourceId); |
| } |
| } |
| |
| List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues(); |
| |
| for (int i = 0; i < safetySourceIssues.size(); i++) { |
| SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i); |
| int issueSeverityLevel = safetySourceIssue.getSeverityLevel(); |
| if (issueSeverityLevel > safetySource.getMaxSeverityLevel()) { |
| throw new IllegalArgumentException( |
| "Unexpected severity level: " |
| + issueSeverityLevel |
| + ", for issue in safety source: " |
| + safetySourceId); |
| } |
| |
| int issueCategory = safetySourceIssue.getIssueCategory(); |
| if (!SafetyCenterFlags.isIssueCategoryAllowedForSource(issueCategory, safetySourceId)) { |
| throw new IllegalArgumentException( |
| "Unexpected issue category: " |
| + issueCategory |
| + ", for issue in safety source: " |
| + safetySourceId); |
| } |
| } |
| |
| return mSafetyCenterConfigReader.isExternalSafetySourceActive(safetySourceId, packageName); |
| } |
| |
| private void validateCallingPackage( |
| SafetySource safetySource, String packageName, String safetySourceId) { |
| if (!packageName.equals(safetySource.getPackageName())) { |
| throw new IllegalArgumentException( |
| "Unexpected package name: " |
| + packageName |
| + ", for safety source: " |
| + safetySourceId); |
| } |
| |
| if (!SdkLevel.isAtLeastU()) { |
| // No more validation checks possible on T devices |
| return; |
| } |
| |
| Set<String> certificateHashes = safetySource.getPackageCertificateHashes(); |
| if (certificateHashes.isEmpty()) { |
| Log.d(TAG, "No cert check requested for package " + packageName); |
| return; |
| } |
| |
| if (!checkCerts(packageName, certificateHashes) |
| && !checkCerts( |
| packageName, |
| SafetyCenterFlags.getAdditionalAllowedPackageCerts(packageName))) { |
| Log.e( |
| TAG, |
| "Package " |
| + packageName |
| + " for source " |
| + safetySourceId |
| + " signed with invalid signature"); |
| throw new IllegalArgumentException("Invalid signature for package " + packageName); |
| } |
| } |
| |
| private boolean checkCerts(String packageName, Set<String> certificateHashes) { |
| boolean hasMatchingCert = false; |
| for (String certHash : certificateHashes) { |
| try { |
| byte[] certificate = new Signature(certHash).toByteArray(); |
| if (mPackageManager.hasSigningCertificate( |
| packageName, certificate, PackageManager.CERT_INPUT_SHA256)) { |
| Log.d(TAG, "Package " + packageName + " has expected signature"); |
| hasMatchingCert = true; |
| } |
| } catch (IllegalArgumentException e) { |
| Log.w(TAG, "Failed to parse signing certificate: " + certHash, e); |
| throw new IllegalStateException( |
| "Failed to parse signing certificate: " + certHash, e); |
| } |
| } |
| return hasMatchingCert; |
| } |
| |
| private boolean processSafetyEvent( |
| String safetySourceId, |
| SafetyEvent safetyEvent, |
| @UserIdInt int userId, |
| boolean isError) { |
| int type = safetyEvent.getType(); |
| switch (type) { |
| case SafetyEvent.SAFETY_EVENT_TYPE_REFRESH_REQUESTED: |
| String refreshBroadcastId = safetyEvent.getRefreshBroadcastId(); |
| if (refreshBroadcastId == null) { |
| Log.w( |
| TAG, |
| "Received safety event of type " |
| + safetyEvent.getType() |
| + " without a refresh broadcast id"); |
| return false; |
| } |
| return mSafetyCenterRefreshTracker.reportSourceRefreshCompleted( |
| refreshBroadcastId, safetySourceId, userId, !isError); |
| case SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED: |
| case SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED: |
| String safetySourceIssueId = safetyEvent.getSafetySourceIssueId(); |
| if (safetySourceIssueId == null) { |
| Log.w( |
| TAG, |
| "Received safety event of type " |
| + safetyEvent.getType() |
| + " without a safety source issue id"); |
| return false; |
| } |
| String safetySourceIssueActionId = safetyEvent.getSafetySourceIssueActionId(); |
| if (safetySourceIssueActionId == null) { |
| Log.w( |
| TAG, |
| "Received safety event of type " |
| + safetyEvent.getType() |
| + " without a safety source issue action id"); |
| return false; |
| } |
| SafetyCenterIssueKey safetyCenterIssueKey = |
| SafetyCenterIssueKey.newBuilder() |
| .setSafetySourceId(safetySourceId) |
| .setSafetySourceIssueId(safetySourceIssueId) |
| .setUserId(userId) |
| .build(); |
| SafetyCenterIssueActionId safetyCenterIssueActionId = |
| SafetyCenterIssueActionId.newBuilder() |
| .setSafetyCenterIssueKey(safetyCenterIssueKey) |
| .setSafetySourceIssueActionId(safetySourceIssueActionId) |
| .build(); |
| boolean success = type == SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED; |
| int result = toSystemEventResult(success); |
| return mSafetyCenterInFlightIssueActionRepository |
| .unmarkSafetyCenterIssueActionInFlight( |
| safetyCenterIssueActionId, |
| getSafetySourceIssue(safetyCenterIssueKey), |
| result); |
| case SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED: |
| case SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_LOCALE_CHANGED: |
| case SafetyEvent.SAFETY_EVENT_TYPE_DEVICE_REBOOTED: |
| return false; |
| } |
| Log.w(TAG, "Unexpected SafetyEvent.Type: " + type); |
| return false; |
| } |
| } |