/* * 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 com.android.safetycenter.UserProfileGroup.PROFILE_TYPE_MANAGED; import static com.android.safetycenter.UserProfileGroup.PROFILE_TYPE_PRIMARY; import static com.android.safetycenter.UserProfileGroup.PROFILE_TYPE_PRIVATE; import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; import android.content.res.Resources; import android.safetycenter.config.SafetyCenterConfig; import android.safetycenter.config.SafetySource; import android.safetycenter.config.SafetySourcesGroup; import android.util.ArrayMap; import android.util.Log; import androidx.annotation.Nullable; import com.android.safetycenter.UserProfileGroup.ProfileType; import com.android.safetycenter.config.ParseException; import com.android.safetycenter.config.SafetyCenterConfigParser; import com.android.safetycenter.resources.SafetyCenterResourcesApk; import java.io.InputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.annotation.concurrent.NotThreadSafe; /** * A class that reads the {@link SafetyCenterConfig} and allows overriding it for tests. * *

This class isn't thread safe. Thread safety must be handled by the caller. * * @hide */ @NotThreadSafe public final class SafetyCenterConfigReader { private static final String TAG = "SafetyCenterConfigReade"; private final SafetyCenterResourcesApk mSafetyCenterResourcesApk; @Nullable private SafetyCenterConfigInternal mConfigInternalFromXml; @Nullable private SafetyCenterConfigInternal mConfigInternalOverrideForTests; /** Creates a {@link SafetyCenterConfigReader} from a {@link SafetyCenterResourcesApk}. */ SafetyCenterConfigReader(SafetyCenterResourcesApk safetyCenterResourcesApk) { mSafetyCenterResourcesApk = safetyCenterResourcesApk; } /** * Loads the {@link SafetyCenterConfig} from the XML file defined in {@code * safety_center_config.xml}; and returns whether this was successful. * *

This method must be called prior to any other call to this class. This call must also be * successful; interacting with this class requires checking that the boolean value returned by * this method was {@code true}. */ boolean loadConfig() { SafetyCenterConfig safetyCenterConfig = loadSafetyCenterConfig(); if (safetyCenterConfig == null) { return false; } mConfigInternalFromXml = SafetyCenterConfigInternal.from(safetyCenterConfig); return true; } /** * Sets an override {@link SafetyCenterConfig} for tests. * *

When set, information provided by this class will be based on the overridden {@link * SafetyCenterConfig}. */ void setConfigOverrideForTests(SafetyCenterConfig safetyCenterConfig) { mConfigInternalOverrideForTests = SafetyCenterConfigInternal.from(safetyCenterConfig); } /** * Clears the {@link SafetyCenterConfig} override set by {@link * #setConfigOverrideForTests(SafetyCenterConfig)}, if any. */ void clearConfigOverrideForTests() { mConfigInternalOverrideForTests = null; } /** Returns the currently active {@link SafetyCenterConfig}. */ SafetyCenterConfig getSafetyCenterConfig() { return getCurrentConfigInternal().getSafetyCenterConfig(); } /** Returns the groups of {@link SafetySource}, in the order expected by the UI. */ public List getSafetySourcesGroups() { return getCurrentConfigInternal().getSafetyCenterConfig().getSafetySourcesGroups(); } /** * Returns the groups of {@link SafetySource}, filtering out any sources where {@link * SafetySources#isLoggable(SafetySource)} is {@code false} (and any resulting empty groups). */ public List getLoggableSafetySourcesGroups() { return getCurrentConfigInternal().getLoggableSourcesGroups(); } /** * Returns the {@link ExternalSafetySource} associated with the {@code safetySourceId}, if any. * *

The returned {@link SafetySource} can either be associated with the XML or overridden * {@link SafetyCenterConfig}; {@link #isExternalSafetySourceActive(String, String)} can be used * to check if it is associated with the current {@link SafetyCenterConfig}. This is to continue * allowing sources from the XML config to interact with SafetCenter during tests (but their * calls will be no-oped). * *

The {@code callingPackageName} can help break the tie when the source is available in both * the overridden config and the "real" config. Otherwise, the test config is preferred. This is * to support overriding "real" sources in tests while ensuring package checks continue to pass * for "real" sources that interact with our APIs. */ @Nullable public ExternalSafetySource getExternalSafetySource( String safetySourceId, String callingPackageName) { SafetyCenterConfigInternal testConfig = mConfigInternalOverrideForTests; SafetyCenterConfigInternal xmlConfig = requireNonNull(mConfigInternalFromXml); if (testConfig == null) { // No override, access source directly. return xmlConfig.getExternalSafetySources().get(safetySourceId); } ExternalSafetySource externalSafetySourceInTestConfig = testConfig.getExternalSafetySources().get(safetySourceId); ExternalSafetySource externalSafetySourceInRealConfig = xmlConfig.getExternalSafetySources().get(safetySourceId); if (externalSafetySourceInTestConfig != null && Objects.equals( externalSafetySourceInTestConfig.getSafetySource().getPackageName(), callingPackageName)) { return externalSafetySourceInTestConfig; } if (externalSafetySourceInRealConfig != null && Objects.equals( externalSafetySourceInRealConfig.getSafetySource().getPackageName(), callingPackageName)) { return externalSafetySourceInRealConfig; } if (externalSafetySourceInTestConfig != null) { return externalSafetySourceInTestConfig; } return externalSafetySourceInRealConfig; } /** * Returns whether the {@code safetySourceId} is associated with an {@link ExternalSafetySource} * that is currently active. * *

The source may either be "active" or "inactive". An active source is a source that is * currently expected to interact with our API and may affect Safety Center status. An inactive * source is expected to interact with Safety Center, but is currently being silenced / no-ops * while an override for tests is in place. * *

The {@code callingPackageName} can be used to differentiate a real source being * overridden. It could be that a test is overriding a real source and as such the real source * should not be able to provide data while its override is in place. */ public boolean isExternalSafetySourceActive( String safetySourceId, @Nullable String callingPackageName) { ExternalSafetySource externalSafetySourceInCurrentConfig = getCurrentConfigInternal().getExternalSafetySources().get(safetySourceId); if (externalSafetySourceInCurrentConfig == null) { return false; } if (callingPackageName == null) { return true; } return Objects.equals( externalSafetySourceInCurrentConfig.getSafetySource().getPackageName(), callingPackageName); } /** * Returns whether the {@code safetySourceId} is associated with an {@link ExternalSafetySource} * that is in the real config XML file (i.e. not being overridden). */ public boolean isExternalSafetySourceFromRealConfig(String safetySourceId) { return requireNonNull(mConfigInternalFromXml) .getExternalSafetySources() .containsKey(safetySourceId); } /** * Returns the {@link Broadcast} defined in the {@link SafetyCenterConfig}, with all the sources * that they should handle and the profile on which they should be dispatched. */ List getBroadcasts() { return getCurrentConfigInternal().getBroadcasts(); } private SafetyCenterConfigInternal getCurrentConfigInternal() { // We require the XML config must be loaded successfully for SafetyCenterManager APIs to // function, regardless of whether the config is subsequently overridden. requireNonNull(mConfigInternalFromXml); if (mConfigInternalOverrideForTests == null) { return mConfigInternalFromXml; } return mConfigInternalOverrideForTests; } @Nullable private SafetyCenterConfig loadSafetyCenterConfig() { InputStream in = mSafetyCenterResourcesApk.getSafetyCenterConfig(); if (in == null) { Log.e(TAG, "Cannot access Safety Center config file"); return null; } Resources resources = mSafetyCenterResourcesApk.getResources(); try { SafetyCenterConfig safetyCenterConfig = SafetyCenterConfigParser.parseXmlResource(in, resources); Log.d(TAG, "SafetyCenterConfig loaded successfully"); return safetyCenterConfig; } catch (ParseException e) { Log.e(TAG, "Cannot parse SafetyCenterConfig", e); return null; } } /** Dumps state for debugging purposes. */ void dump(PrintWriter fout) { fout.println("XML CONFIG"); fout.println("\t" + mConfigInternalFromXml); fout.println(); fout.println("OVERRIDE CONFIG"); fout.println("\t" + mConfigInternalOverrideForTests); fout.println(); } /** A wrapper class around the parsed XML config. */ private static final class SafetyCenterConfigInternal { private final SafetyCenterConfig mConfig; private final ArrayMap mExternalSafetySources; private final List mLoggableSourcesGroups; private final List mBroadcasts; private SafetyCenterConfigInternal( SafetyCenterConfig safetyCenterConfig, ArrayMap externalSafetySources, List loggableSourcesGroups, List broadcasts) { mConfig = safetyCenterConfig; mExternalSafetySources = externalSafetySources; mLoggableSourcesGroups = loggableSourcesGroups; mBroadcasts = broadcasts; } private SafetyCenterConfig getSafetyCenterConfig() { return mConfig; } private ArrayMap getExternalSafetySources() { return mExternalSafetySources; } private List getLoggableSourcesGroups() { return mLoggableSourcesGroups; } private List getBroadcasts() { return mBroadcasts; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof SafetyCenterConfigInternal)) return false; SafetyCenterConfigInternal configInternal = (SafetyCenterConfigInternal) o; return mConfig.equals(configInternal.mConfig); } @Override public int hashCode() { return Objects.hash(mConfig); } @Override public String toString() { return "SafetyCenterConfigInternal{" + "mConfig=" + mConfig + ", mExternalSafetySources=" + mExternalSafetySources + ", mLoggableSourcesGroups=" + mLoggableSourcesGroups + ", mBroadcasts=" + mBroadcasts + '}'; } private static SafetyCenterConfigInternal from(SafetyCenterConfig safetyCenterConfig) { return new SafetyCenterConfigInternal( safetyCenterConfig, extractExternalSafetySources(safetyCenterConfig), extractLoggableSafetySourcesGroups(safetyCenterConfig), unmodifiableList(extractBroadcasts(safetyCenterConfig))); } private static ArrayMap extractExternalSafetySources( SafetyCenterConfig safetyCenterConfig) { ArrayMap externalSafetySources = new ArrayMap<>(); 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 (!SafetySources.isExternal(safetySource)) { continue; } boolean hasEntryInStatelessGroup = safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC && safetySourcesGroup.getType() == SafetySourcesGroup .SAFETY_SOURCES_GROUP_TYPE_STATELESS; externalSafetySources.put( safetySource.getId(), new ExternalSafetySource(safetySource, hasEntryInStatelessGroup)); } } return externalSafetySources; } private static List extractLoggableSafetySourcesGroups( SafetyCenterConfig safetyCenterConfig) { List originalGroups = safetyCenterConfig.getSafetySourcesGroups(); List filteredGroups = new ArrayList<>(originalGroups.size()); for (int i = 0; i < originalGroups.size(); i++) { SafetySourcesGroup originalGroup = originalGroups.get(i); SafetySourcesGroup.Builder filteredGroupBuilder = SafetySourcesGroups.copyToBuilderWithoutSources(originalGroup); List originalSources = originalGroup.getSafetySources(); for (int j = 0; j < originalSources.size(); j++) { SafetySource source = originalSources.get(j); if (SafetySources.isLoggable(source)) { filteredGroupBuilder.addSafetySource(source); } } SafetySourcesGroup filteredGroup = filteredGroupBuilder.build(); if (!filteredGroup.getSafetySources().isEmpty()) { filteredGroups.add(filteredGroup); } } return filteredGroups; } private static List extractBroadcasts(SafetyCenterConfig safetyCenterConfig) { ArrayMap packageNameToBroadcast = new ArrayMap<>(); List broadcasts = new ArrayList<>(); 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 (!SafetySources.isExternal(safetySource)) { continue; } Broadcast broadcast = packageNameToBroadcast.get(safetySource.getPackageName()); if (broadcast == null) { broadcast = new Broadcast(safetySource.getPackageName()); packageNameToBroadcast.put(safetySource.getPackageName(), broadcast); broadcasts.add(broadcast); } broadcast.mSourceIdsForProfileParent.add(safetySource.getId()); if (safetySource.isRefreshOnPageOpenAllowed()) { broadcast.mSourceIdsForProfileParentOnPageOpen.add(safetySource.getId()); } boolean needsManagedProfilesBroadcast = SafetySources.supportsProfileType(safetySource, PROFILE_TYPE_MANAGED); if (needsManagedProfilesBroadcast) { broadcast.mSourceIdsForManagedProfiles.add(safetySource.getId()); if (safetySource.isRefreshOnPageOpenAllowed()) { broadcast.mSourceIdsForManagedProfilesOnPageOpen.add( safetySource.getId()); } } // TODO(b/317378205): think about generalising these fields in Broadcast so that // we are not duplicating the code - it can be a source of confusion and errors // in future. boolean needsPrivateProfileBroadcast = SafetySources.supportsProfileType(safetySource, PROFILE_TYPE_PRIVATE); if (needsPrivateProfileBroadcast) { broadcast.mSourceIdsForPrivateProfile.add(safetySource.getId()); if (safetySource.isRefreshOnPageOpenAllowed()) { broadcast.mSourceIdsForPrivateProfileOnPageOpen.add( safetySource.getId()); } } } } return broadcasts; } } /** * A wrapper class around a {@link SafetySource} that is providing data externally. * * @hide */ public static final class ExternalSafetySource { private final SafetySource mSafetySource; private final boolean mHasEntryInStatelessGroup; private ExternalSafetySource(SafetySource safetySource, boolean hasEntryInStatelessGroup) { mSafetySource = safetySource; mHasEntryInStatelessGroup = hasEntryInStatelessGroup; } /** Returns the external {@link SafetySource}. */ public SafetySource getSafetySource() { return mSafetySource; } /** * Returns whether the external {@link SafetySource} has an entry in a stateless {@link * SafetySourcesGroup}. */ public boolean hasEntryInStatelessGroup() { return mHasEntryInStatelessGroup; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ExternalSafetySource)) return false; ExternalSafetySource that = (ExternalSafetySource) o; return mHasEntryInStatelessGroup == that.mHasEntryInStatelessGroup && mSafetySource.equals(that.mSafetySource); } @Override public int hashCode() { return Objects.hash(mSafetySource, mHasEntryInStatelessGroup); } @Override public String toString() { return "ExternalSafetySource{" + "mSafetySource=" + mSafetySource + ", mHasEntryInStatelessGroup=" + mHasEntryInStatelessGroup + '}'; } } /** A class that represents a broadcast to be sent to safety sources. */ static final class Broadcast { private final String mPackageName; private final List mSourceIdsForProfileParent = new ArrayList<>(); private final List mSourceIdsForProfileParentOnPageOpen = new ArrayList<>(); private final List mSourceIdsForManagedProfiles = new ArrayList<>(); private final List mSourceIdsForManagedProfilesOnPageOpen = new ArrayList<>(); private final List mSourceIdsForPrivateProfile = new ArrayList<>(); private final List mSourceIdsForPrivateProfileOnPageOpen = new ArrayList<>(); private Broadcast(String packageName) { mPackageName = packageName; } /** Returns the package name to dispatch the broadcast to. */ String getPackageName() { return mPackageName; } /** * Returns the safety source ids associated with this broadcast in the given profile type. * *

If this list is empty, there are no sources to dispatch to in the given profile type. */ List getSourceIdsForProfileType(@ProfileType int profileType) { switch (profileType) { case PROFILE_TYPE_PRIMARY: return unmodifiableList(mSourceIdsForProfileParent); case PROFILE_TYPE_MANAGED: return unmodifiableList(mSourceIdsForManagedProfiles); case PROFILE_TYPE_PRIVATE: return unmodifiableList(mSourceIdsForPrivateProfile); default: Log.w(TAG, "source ids asked for unexpected profile " + profileType); return emptyList(); } } /** * Returns the safety source ids associated with this broadcast in the given profile type * that have refreshOnPageOpenAllowed set to true in the XML config. * *

If this list is empty, there are no sources to dispatch to in the given profile type. */ List getSourceIdsOnPageOpenForProfileType(@ProfileType int profileType) { switch (profileType) { case PROFILE_TYPE_PRIMARY: return unmodifiableList(mSourceIdsForProfileParentOnPageOpen); case PROFILE_TYPE_MANAGED: return unmodifiableList(mSourceIdsForManagedProfilesOnPageOpen); case PROFILE_TYPE_PRIVATE: return unmodifiableList(mSourceIdsForPrivateProfileOnPageOpen); default: Log.w(TAG, "source ids asked for unexpected profile " + profileType); return emptyList(); } } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Broadcast)) return false; Broadcast that = (Broadcast) o; return mPackageName.equals(that.mPackageName) && mSourceIdsForProfileParent.equals(that.mSourceIdsForProfileParent) && mSourceIdsForProfileParentOnPageOpen.equals( that.mSourceIdsForProfileParentOnPageOpen) && mSourceIdsForManagedProfiles.equals(that.mSourceIdsForManagedProfiles) && mSourceIdsForManagedProfilesOnPageOpen.equals( that.mSourceIdsForManagedProfilesOnPageOpen) && mSourceIdsForPrivateProfile.equals(that.mSourceIdsForPrivateProfile) && mSourceIdsForPrivateProfileOnPageOpen.equals( that.mSourceIdsForPrivateProfileOnPageOpen); } @Override public int hashCode() { return Objects.hash( mPackageName, mSourceIdsForProfileParent, mSourceIdsForProfileParentOnPageOpen, mSourceIdsForManagedProfiles, mSourceIdsForManagedProfilesOnPageOpen, mSourceIdsForPrivateProfile, mSourceIdsForPrivateProfileOnPageOpen); } @Override public String toString() { return "Broadcast{" + "mPackageName='" + mPackageName + "', mSourceIdsForProfileParent=" + mSourceIdsForProfileParent + ", mSourceIdsForProfileParentOnPageOpen=" + mSourceIdsForProfileParentOnPageOpen + ", mSourceIdsForManagedProfiles=" + mSourceIdsForManagedProfiles + ", mSourceIdsForManagedProfilesOnPageOpen=" + mSourceIdsForManagedProfilesOnPageOpen + ", mSourceIdsForPrivateProfile=" + mSourceIdsForPrivateProfile + ", mSourceIdsForPrivateProfileOnPageOpen=" + mSourceIdsForPrivateProfileOnPageOpen + '}'; } } }