unplugged-system/packages/modules/Permission/service/java/com/android/safetycenter/SafetyCenterFlags.java

577 lines
24 KiB
Java

/*
* 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 android.safetycenter.SafetyCenterManager.RefreshReason;
import android.annotation.Nullable;
import android.os.Binder;
import android.provider.DeviceConfig;
import android.safetycenter.SafetySourceData;
import android.safetycenter.SafetySourceIssue;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.modules.utils.build.SdkLevel;
import com.android.safetycenter.resources.SafetyCenterResourcesContext;
import java.io.PrintWriter;
import java.time.Duration;
/**
* A class to access the Safety Center {@link DeviceConfig} flags.
*
* @hide
*/
@RequiresApi(TIRAMISU)
public final class SafetyCenterFlags {
private static final String TAG = "SafetyCenterFlags";
/** {@link DeviceConfig} property name for {@link #getSafetyCenterEnabled()}. */
static final String PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled";
private static final String PROPERTY_NOTIFICATIONS_ENABLED =
"safety_center_notifications_enabled";
private static final String PROPERTY_NOTIFICATIONS_ALLOWED_SOURCES =
"safety_center_notifications_allowed_sources";
private static final String PROPERTY_NOTIFICATIONS_MIN_DELAY =
"safety_center_notifications_min_delay";
private static final String PROPERTY_NOTIFICATIONS_IMMEDIATE_BEHAVIOR_ISSUES =
"safety_center_notifications_immediate_behavior_issues";
private static final String PROPERTY_NOTIFICATION_RESURFACE_INTERVAL =
"safety_center_notification_resurface_interval";
private static final String PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT =
"safety_center_show_error_entries_on_timeout";
private static final String PROPERTY_REPLACE_LOCK_SCREEN_ICON_ACTION =
"safety_center_replace_lock_screen_icon_action";
private static final String PROPERTY_RESOLVING_ACTION_TIMEOUT_MILLIS =
"safety_center_resolve_action_timeout_millis";
private static final String PROPERTY_FGS_ALLOWLIST_DURATION_MILLIS =
"safety_center_refresh_fgs_allowlist_duration_millis";
private static final String PROPERTY_RESURFACE_ISSUE_MAX_COUNTS =
"safety_center_resurface_issue_max_counts";
private static final String PROPERTY_RESURFACE_ISSUE_DELAYS_MILLIS =
"safety_center_resurface_issue_delays_millis";
private static final String PROPERTY_UNTRACKED_SOURCES = "safety_center_untracked_sources";
private static final String PROPERTY_BACKGROUND_REFRESH_DENIED_SOURCES =
"safety_center_background_refresh_denied_sources";
private static final String PROPERTY_REFRESH_SOURCES_TIMEOUTS_MILLIS =
"safety_center_refresh_sources_timeouts_millis";
private static final String PROPERTY_ISSUE_CATEGORY_ALLOWLISTS =
"safety_center_issue_category_allowlists";
private static final String PROPERTY_ALLOW_STATSD_LOGGING =
"safety_center_allow_statsd_logging";
private static final String PROPERTY_SHOW_SUBPAGES = "safety_center_show_subpages";
private static final String PROPERTY_OVERRIDE_REFRESH_ON_PAGE_OPEN_SOURCES =
"safety_center_override_refresh_on_page_open_sources";
private static final String PROPERTY_ADDITIONAL_ALLOW_PACKAGE_CERTS =
"safety_center_additional_allow_package_certs";
private static final Duration FGS_ALLOWLIST_DEFAULT_DURATION = Duration.ofSeconds(20);
private static final String PROPERTY_TEMP_HIDDEN_ISSUE_RESURFACE_DELAY_MILLIS =
"safety_center_temp_hidden_issue_resurface_delay_millis";
private static final Duration RESOLVING_ACTION_TIMEOUT_DEFAULT_DURATION =
Duration.ofSeconds(10);
private static final Duration NOTIFICATIONS_MIN_DELAY_DEFAULT_DURATION = Duration.ofDays(180);
private static final String REFRESH_SOURCES_TIMEOUT_DEFAULT =
"100:15000,200:60000,300:30000,400:30000,500:30000,600:3600000";
private static final Duration REFRESH_SOURCES_TIMEOUT_DEFAULT_DURATION = Duration.ofSeconds(15);
private static final String RESURFACE_ISSUE_MAX_COUNT_DEFAULT = "200:0,300:1,400:1";
private static final long RESURFACE_ISSUE_MAX_COUNT_DEFAULT_COUNT = 0;
private static final String RESURFACE_ISSUE_DELAYS_DEFAULT = "";
private static final Duration RESURFACE_ISSUE_DELAYS_DEFAULT_DURATION = Duration.ofDays(180);
private static volatile String sUntrackedSourcesDefault =
"AndroidAccessibility,AndroidBackgroundLocation,"
+ "AndroidNotificationListener,AndroidPermissionAutoRevoke";
private static volatile String sBackgroundRefreshDenyDefault = "";
private static volatile String sIssueCategoryAllowlistDefault = "";
private static volatile String sRefreshOnPageOpenSourcesDefault =
"AndroidBiometrics,AndroidLockScreen";
static void init(SafetyCenterResourcesContext resourceContext) {
String untrackedSourcesDefault =
resourceContext.getOptionalStringByName("config_defaultUntrackedSources");
if (untrackedSourcesDefault != null) {
sUntrackedSourcesDefault = untrackedSourcesDefault;
}
String backgroundRefreshDenyDefault =
resourceContext.getOptionalStringByName("config_defaultBackgroundRefreshDeny");
if (backgroundRefreshDenyDefault != null) {
sBackgroundRefreshDenyDefault = backgroundRefreshDenyDefault;
}
String issueCategoryAllowlistDefault =
resourceContext.getOptionalStringByName("config_defaultIssueCategoryAllowlist");
if (issueCategoryAllowlistDefault != null) {
sIssueCategoryAllowlistDefault = issueCategoryAllowlistDefault;
}
String refreshOnPageOpenSourcesDefault =
resourceContext.getOptionalStringByName("config_defaultRefreshOnPageOpenSources");
if (refreshOnPageOpenSourcesDefault != null) {
sRefreshOnPageOpenSourcesDefault = refreshOnPageOpenSourcesDefault;
}
}
private static final Duration TEMP_HIDDEN_ISSUE_RESURFACE_DELAY_DEFAULT_DURATION =
Duration.ofDays(2);
/** Dumps state for debugging purposes. */
static void dump(PrintWriter fout) {
fout.println("FLAGS");
printFlag(fout, PROPERTY_SAFETY_CENTER_ENABLED, getSafetyCenterEnabled());
printFlag(fout, PROPERTY_NOTIFICATIONS_ENABLED, getNotificationsEnabled());
printFlag(fout, PROPERTY_NOTIFICATIONS_ALLOWED_SOURCES, getNotificationsAllowedSourceIds());
printFlag(fout, PROPERTY_NOTIFICATIONS_MIN_DELAY, getNotificationsMinDelay());
printFlag(
fout,
PROPERTY_NOTIFICATIONS_IMMEDIATE_BEHAVIOR_ISSUES,
getImmediateNotificationBehaviorIssues());
printFlag(
fout, PROPERTY_NOTIFICATION_RESURFACE_INTERVAL, getNotificationResurfaceInterval());
printFlag(fout, PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT, getShowErrorEntriesOnTimeout());
printFlag(fout, PROPERTY_REPLACE_LOCK_SCREEN_ICON_ACTION, getReplaceLockScreenIconAction());
printFlag(fout, PROPERTY_RESOLVING_ACTION_TIMEOUT_MILLIS, getResolvingActionTimeout());
printFlag(fout, PROPERTY_FGS_ALLOWLIST_DURATION_MILLIS, getFgsAllowlistDuration());
printFlag(fout, PROPERTY_UNTRACKED_SOURCES, getUntrackedSourceIds());
printFlag(fout, PROPERTY_RESURFACE_ISSUE_MAX_COUNTS, getResurfaceIssueMaxCounts());
printFlag(fout, PROPERTY_RESURFACE_ISSUE_DELAYS_MILLIS, getResurfaceIssueDelaysMillis());
printFlag(
fout,
PROPERTY_BACKGROUND_REFRESH_DENIED_SOURCES,
getBackgroundRefreshDeniedSourceIds());
printFlag(
fout, PROPERTY_REFRESH_SOURCES_TIMEOUTS_MILLIS, getRefreshSourcesTimeoutsMillis());
printFlag(fout, PROPERTY_ISSUE_CATEGORY_ALLOWLISTS, getIssueCategoryAllowlists());
printFlag(fout, PROPERTY_ALLOW_STATSD_LOGGING, getAllowStatsdLogging());
printFlag(fout, PROPERTY_SHOW_SUBPAGES, getShowSubpages());
printFlag(
fout,
PROPERTY_OVERRIDE_REFRESH_ON_PAGE_OPEN_SOURCES,
getOverrideRefreshOnPageOpenSourceIds());
printFlag(
fout,
PROPERTY_ADDITIONAL_ALLOW_PACKAGE_CERTS,
getAdditionalAllowedPackageCertsString());
fout.println();
}
private static void printFlag(PrintWriter pw, String key, @Nullable Duration duration) {
if (duration == null) {
printFlag(pw, key, "null");
} else {
printFlag(pw, key, duration.toMillis() + " (" + duration + ")");
}
}
private static void printFlag(PrintWriter pw, String key, Object value) {
pw.println("\t" + key + "=" + value);
}
/** Returns whether Safety Center is enabled. */
public static boolean getSafetyCenterEnabled() {
return getBoolean(PROPERTY_SAFETY_CENTER_ENABLED, SdkLevel.isAtLeastU());
}
/** Returns whether Safety Center notifications are enabled. */
public static boolean getNotificationsEnabled() {
return getBoolean(PROPERTY_NOTIFICATIONS_ENABLED, SdkLevel.isAtLeastU());
}
/**
* Returns the IDs of sources that Safety Center can send notifications about, in addition to
* those permitted by the current XML config.
*
* <p>If the ID of a source appears on this list then Safety Center may send notifications about
* issues from that source, regardless of (overriding) the XML config. If the ID of a source is
* absent from this list, then Safety Center may send such notifications only if the XML config
* allows it.
*
* <p>Note that the {@code areNotificationsAllowed} config attribute is only available on API U+
* and therefore this is the only way to enable notifications for sources on Android T.
*/
public static ArraySet<String> getNotificationsAllowedSourceIds() {
return getCommaSeparatedStrings(PROPERTY_NOTIFICATIONS_ALLOWED_SOURCES);
}
/**
* Returns the minimum delay before Safety Center can send a notification for an issue with
* {@link SafetySourceIssue#NOTIFICATION_BEHAVIOR_DELAYED}.
*
* <p>The actual delay used may be longer.
*/
public static Duration getNotificationsMinDelay() {
return getDuration(
PROPERTY_NOTIFICATIONS_MIN_DELAY, NOTIFICATIONS_MIN_DELAY_DEFAULT_DURATION);
}
/**
* Returns the issue type IDs for which, if otherwise undefined, Safety Center should use {@link
* SafetySourceIssue#NOTIFICATION_BEHAVIOR_IMMEDIATELY}.
*
* <p>If a safety source specifies the notification behavior of an issue explicitly this flag
* has no effect, even if the issue matches one of the entries in this flag.
*
* <p>Entries in this set should be strings of the form "safety_source_id/issue_type_id".
*/
public static ArraySet<String> getImmediateNotificationBehaviorIssues() {
return getCommaSeparatedStrings(PROPERTY_NOTIFICATIONS_IMMEDIATE_BEHAVIOR_ISSUES);
}
/**
* Returns the minimum interval that must elapse before Safety Center can resurface a
* notification after it was dismissed, or {@code null} (the default) if dismissed notifications
* cannot resurface.
*
* <p>Returns {@code null} if the underlying device config flag is either unset or is set to a
* negative value.
*
* <p>There may be other conditions for resurfacing a notification and the actual delay may be
* longer than this.
*/
@Nullable
public static Duration getNotificationResurfaceInterval() {
long millis = getLong(PROPERTY_NOTIFICATION_RESURFACE_INTERVAL, -1);
if (millis < 0) {
return null;
} else {
return Duration.ofMillis(millis);
}
}
/**
* Returns whether we should show error entries for sources that timeout when refreshing them.
*/
static boolean getShowErrorEntriesOnTimeout() {
return getBoolean(PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT, true);
}
/**
* Returns whether we should replace the lock screen source's {@link
* android.safetycenter.SafetySourceStatus.IconAction}.
*/
public static boolean getReplaceLockScreenIconAction() {
return getBoolean(PROPERTY_REPLACE_LOCK_SCREEN_ICON_ACTION, true);
}
/**
* Returns the time for which Safety Center will wait for a source to respond to a resolving
* action before timing out.
*/
static Duration getResolvingActionTimeout() {
return getDuration(
PROPERTY_RESOLVING_ACTION_TIMEOUT_MILLIS,
RESOLVING_ACTION_TIMEOUT_DEFAULT_DURATION);
}
/**
* Returns the time for which an app, upon receiving a Safety Center refresh broadcast, will be
* placed on a temporary power allowlist allowing it to start a foreground service from the
* background.
*/
static Duration getFgsAllowlistDuration() {
return getDuration(PROPERTY_FGS_ALLOWLIST_DURATION_MILLIS, FGS_ALLOWLIST_DEFAULT_DURATION);
}
/**
* Returns the IDs of sources that should not be tracked, for example because they are
* mid-rollout. Broadcasts are still sent to these sources.
*/
static ArraySet<String> getUntrackedSourceIds() {
return getCommaSeparatedStrings(PROPERTY_UNTRACKED_SOURCES, sUntrackedSourcesDefault);
}
/**
* Returns the IDs of sources that should only be refreshed when Safety Center is on screen. We
* will refresh these sources only on page open and when the scan button is clicked.
*/
static ArraySet<String> getBackgroundRefreshDeniedSourceIds() {
return getCommaSeparatedStrings(
PROPERTY_BACKGROUND_REFRESH_DENIED_SOURCES, sBackgroundRefreshDenyDefault);
}
/**
* Returns the time for which a Safety Center refresh is allowed to wait for a source to respond
* to a refresh request before timing out and marking the refresh as completed, based on the
* reason for the refresh.
*/
static Duration getRefreshSourcesTimeout(@RefreshReason int refreshReason) {
String refreshSourcesTimeouts = getRefreshSourcesTimeoutsMillis();
Long timeout = getLongValueFromStringMapping(refreshSourcesTimeouts, refreshReason);
if (timeout != null) {
return Duration.ofMillis(timeout);
}
return REFRESH_SOURCES_TIMEOUT_DEFAULT_DURATION;
}
/**
* Returns a comma-delimited list of colon-delimited pairs where the left value is a {@link
* RefreshReason} and the right value is the refresh timeout applied for each source in case of
* a refresh.
*/
private static String getRefreshSourcesTimeoutsMillis() {
return getString(PROPERTY_REFRESH_SOURCES_TIMEOUTS_MILLIS, REFRESH_SOURCES_TIMEOUT_DEFAULT);
}
/**
* Returns the number of times an issue of the given {@link SafetySourceData.SeverityLevel}
* should be resurfaced.
*/
public static long getResurfaceIssueMaxCount(
@SafetySourceData.SeverityLevel int severityLevel) {
String maxCountsConfigString = getResurfaceIssueMaxCounts();
Long maxCount = getLongValueFromStringMapping(maxCountsConfigString, severityLevel);
if (maxCount != null) {
return maxCount;
}
return RESURFACE_ISSUE_MAX_COUNT_DEFAULT_COUNT;
}
/**
* Returns a comma-delimited list of colon-delimited pairs where the left value is an issue
* {@link SafetySourceData.SeverityLevel} and the right value is the number of times an issue of
* this {@link SafetySourceData.SeverityLevel} should be resurfaced.
*/
private static String getResurfaceIssueMaxCounts() {
return getString(PROPERTY_RESURFACE_ISSUE_MAX_COUNTS, RESURFACE_ISSUE_MAX_COUNT_DEFAULT);
}
/**
* Returns the time after which a dismissed issue of the given {@link
* SafetySourceData.SeverityLevel} will resurface if it has not reached the maximum count for
* which a dismissed issue of the given {@link SafetySourceData.SeverityLevel} should be
* resurfaced.
*/
public static Duration getResurfaceIssueDelay(
@SafetySourceData.SeverityLevel int severityLevel) {
String delaysConfigString = getResurfaceIssueDelaysMillis();
Long delayMillis = getLongValueFromStringMapping(delaysConfigString, severityLevel);
if (delayMillis != null) {
return Duration.ofMillis(delayMillis);
}
return RESURFACE_ISSUE_DELAYS_DEFAULT_DURATION;
}
/**
* Returns a comma-delimited list of colon-delimited pairs where the left value is an issue
* {@link SafetySourceData.SeverityLevel} and the right value is the time after which a
* dismissed issue of this safety source severity level will resurface if it has not reached the
* maximum count for which a dismissed issue of this {@link SafetySourceData.SeverityLevel}
* should be resurfaced.
*/
private static String getResurfaceIssueDelaysMillis() {
return getString(PROPERTY_RESURFACE_ISSUE_DELAYS_MILLIS, RESURFACE_ISSUE_DELAYS_DEFAULT);
}
/** Returns a duration after which a temporarily hidden issue will resurface. */
public static Duration getTemporarilyHiddenIssueResurfaceDelay() {
return getDuration(
PROPERTY_TEMP_HIDDEN_ISSUE_RESURFACE_DELAY_MILLIS,
TEMP_HIDDEN_ISSUE_RESURFACE_DELAY_DEFAULT_DURATION);
}
/**
* Returns whether a safety source is allowed to send issues for the given {@link
* SafetySourceIssue.IssueCategory}.
*/
public static boolean isIssueCategoryAllowedForSource(
@SafetySourceIssue.IssueCategory int issueCategory, String safetySourceId) {
String issueCategoryAllowlists = getIssueCategoryAllowlists();
String allowlistString =
getStringValueFromStringMapping(issueCategoryAllowlists, issueCategory);
if (allowlistString == null) {
return true;
}
String[] allowlistArray = allowlistString.split("\\|");
for (int i = 0; i < allowlistArray.length; i++) {
if (allowlistArray[i].equals(safetySourceId)) {
return true;
}
}
return false;
}
/** Returns a set of package certificates allowlisted for the given package name. */
public static ArraySet<String> getAdditionalAllowedPackageCerts(String packageName) {
String property = getAdditionalAllowedPackageCertsString();
String allowlistedCertString = getStringValueFromStringMapping(property, packageName);
if (allowlistedCertString == null) {
return new ArraySet<>();
}
return new ArraySet<String>(allowlistedCertString.split("\\|"));
}
/**
* Returns a comma-delimited list of colon-delimited pairs where the left value is an issue
* {@link SafetySourceIssue.IssueCategory} and the right value is a vertical-bar-delimited list
* of IDs of safety sources that are allowed to send issues with this category.
*/
private static String getIssueCategoryAllowlists() {
return getString(PROPERTY_ISSUE_CATEGORY_ALLOWLISTS, sIssueCategoryAllowlistDefault);
}
private static String getAdditionalAllowedPackageCertsString() {
return getString(PROPERTY_ADDITIONAL_ALLOW_PACKAGE_CERTS, "");
}
/** Returns whether we allow statsd logging. */
public static boolean getAllowStatsdLogging() {
return getBoolean(PROPERTY_ALLOW_STATSD_LOGGING, true);
}
/**
* Returns whether to show subpages in the Safety Center UI for Android-U instead of the
* expand-and-collapse list implementation.
*/
static boolean getShowSubpages() {
return SdkLevel.isAtLeastU() && getBoolean(PROPERTY_SHOW_SUBPAGES, true);
}
/**
* Returns an array of safety source Ids that will be refreshed on page open, even if
* refreshOnPageOpenAllowed is false (the default) in the XML config.
*/
static ArraySet<String> getOverrideRefreshOnPageOpenSourceIds() {
return getCommaSeparatedStrings(
PROPERTY_OVERRIDE_REFRESH_ON_PAGE_OPEN_SOURCES, sRefreshOnPageOpenSourcesDefault);
}
private static Duration getDuration(String property, Duration defaultValue) {
return Duration.ofMillis(getLong(property, defaultValue.toMillis()));
}
private static boolean getBoolean(String property, boolean defaultValue) {
// This call requires the READ_DEVICE_CONFIG permission.
final long callingId = Binder.clearCallingIdentity();
try {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, property, defaultValue);
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
private static long getLong(String property, long defaultValue) {
// This call requires the READ_DEVICE_CONFIG permission.
final long callingId = Binder.clearCallingIdentity();
try {
return DeviceConfig.getLong(DeviceConfig.NAMESPACE_PRIVACY, property, defaultValue);
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
private static ArraySet<String> getCommaSeparatedStrings(String property) {
return getCommaSeparatedStrings(property, "");
}
private static ArraySet<String> getCommaSeparatedStrings(String property, String defaultValue) {
return new ArraySet<>(getString(property, defaultValue).split(","));
}
private static String getString(String property, String defaultValue) {
// This call requires the READ_DEVICE_CONFIG permission.
final long callingId = Binder.clearCallingIdentity();
try {
return DeviceConfig.getString(DeviceConfig.NAMESPACE_PRIVACY, property, defaultValue);
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
/**
* Gets a long value for the provided integer key in a comma separated list of colon separated
* pairs of integers and longs.
*/
@Nullable
private static Long getLongValueFromStringMapping(String config, int key) {
String valueString = getStringValueFromStringMapping(config, key);
if (valueString == null) {
return null;
}
try {
return Long.parseLong(valueString);
} catch (NumberFormatException e) {
Log.w(TAG, "Badly formatted string config: " + config, e);
return null;
}
}
/**
* Gets a value for the provided integer key in a comma separated list of colon separated pairs
* of integers and strings.
*/
@Nullable
private static String getStringValueFromStringMapping(String config, int key) {
return getStringValueFromStringMapping(config, Integer.toString(key));
}
/**
* Gets a value for the provided key in a comma separated list of colon separated key-value
* string pairs.
*/
@Nullable
private static String getStringValueFromStringMapping(String config, String key) {
if (config.isEmpty()) {
return null;
}
String[] pairsList = config.split(",");
for (int i = 0; i < pairsList.length; i++) {
String[] pair = pairsList[i].split(":", -1 /* allow trailing empty strings */);
if (pair.length != 2) {
Log.w(TAG, "Badly formatted string config: " + config);
continue;
}
if (pair[0].equals(key)) {
return pair[1];
}
}
return null;
}
private SafetyCenterFlags() {}
}