summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--framework-s/java/android/safetycenter/SafetySourceIssue.java2
-rw-r--r--service/java/com/android/safetycenter/PendingIntentFactory.java2
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterFlags.java79
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterService.java14
-rw-r--r--service/java/com/android/safetycenter/data/AndroidLockScreenFix.java148
-rw-r--r--service/java/com/android/safetycenter/data/DefaultActionOverrideFix.java180
-rw-r--r--service/java/com/android/safetycenter/data/SafetySourceDataFix.java76
-rw-r--r--service/java/com/android/safetycenter/data/SafetySourceDataOverrides.java81
-rw-r--r--tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt42
-rw-r--r--tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetySourceDataFixesTest.kt298
-rw-r--r--tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt17
-rw-r--r--tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetySourceTestData.kt25
12 files changed, 791 insertions, 173 deletions
diff --git a/framework-s/java/android/safetycenter/SafetySourceIssue.java b/framework-s/java/android/safetycenter/SafetySourceIssue.java
index de75aa298..b6e291fe3 100644
--- a/framework-s/java/android/safetycenter/SafetySourceIssue.java
+++ b/framework-s/java/android/safetycenter/SafetySourceIssue.java
@@ -956,6 +956,8 @@ public final class SafetySourceIssue implements Parcelable {
mConfirmationDialogDetails = action.mConfirmationDialogDetails;
}
+ // TODO(b/303443020): Add setters for id, label, and pendingIntent
+
/**
* Sets whether the action will resolve the safety issue. Defaults to {@code false}.
*
diff --git a/service/java/com/android/safetycenter/PendingIntentFactory.java b/service/java/com/android/safetycenter/PendingIntentFactory.java
index a857a07cb..c4e9decd2 100644
--- a/service/java/com/android/safetycenter/PendingIntentFactory.java
+++ b/service/java/com/android/safetycenter/PendingIntentFactory.java
@@ -71,7 +71,7 @@ public final class PendingIntentFactory {
* is no valid target for the given {@code intentAction}.
*/
@Nullable
- PendingIntent getPendingIntent(
+ public PendingIntent getPendingIntent(
String sourceId,
@Nullable String intentAction,
String packageName,
diff --git a/service/java/com/android/safetycenter/SafetyCenterFlags.java b/service/java/com/android/safetycenter/SafetyCenterFlags.java
index 821987ce9..67c4d25d6 100644
--- a/service/java/com/android/safetycenter/SafetyCenterFlags.java
+++ b/service/java/com/android/safetycenter/SafetyCenterFlags.java
@@ -32,6 +32,9 @@ import com.android.safetycenter.resources.SafetyCenterResourcesApk;
import java.io.PrintWriter;
import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
/**
* A class to access the Safety Center {@link DeviceConfig} flags.
@@ -102,6 +105,9 @@ public final class SafetyCenterFlags {
private static final String PROPERTY_TEMP_HIDDEN_ISSUE_RESURFACE_DELAY_MILLIS =
"safety_center_temp_hidden_issue_resurface_delay_millis";
+ private static final String PROPERTY_ACTIONS_TO_OVERRIDE_WITH_DEFAULT_INTENT =
+ "safety_center_actions_to_override_with_default_intent";
+
private static final Duration RESOLVING_ACTION_TIMEOUT_DEFAULT_DURATION =
Duration.ofSeconds(10);
@@ -127,6 +133,8 @@ public final class SafetyCenterFlags {
private static volatile String sRefreshOnPageOpenSourcesDefault = "AndroidBiometrics";
+ private static volatile String sActionsToOverrideWithDefaultIntentDefault = "";
+
static void init(SafetyCenterResourcesApk safetyCenterResourcesApk) {
String untrackedSourcesDefault =
safetyCenterResourcesApk.getOptionalStringByName("config_defaultUntrackedSources");
@@ -151,6 +159,12 @@ public final class SafetyCenterFlags {
if (refreshOnPageOpenSourcesDefault != null) {
sRefreshOnPageOpenSourcesDefault = refreshOnPageOpenSourcesDefault;
}
+ String actionsToOverrideWithDefaultIntentDefault =
+ safetyCenterResourcesApk.getOptionalStringByName(
+ "config_defaultActionsToOverrideWithDefaultIntent");
+ if (actionsToOverrideWithDefaultIntentDefault != null) {
+ sActionsToOverrideWithDefaultIntentDefault = actionsToOverrideWithDefaultIntentDefault;
+ }
}
private static final Duration TEMP_HIDDEN_ISSUE_RESURFACE_DELAY_DEFAULT_DURATION =
@@ -395,6 +409,17 @@ public final class SafetyCenterFlags {
return getString(PROPERTY_RESURFACE_ISSUE_DELAYS_MILLIS, RESURFACE_ISSUE_DELAYS_DEFAULT);
}
+ /**
+ * Returns a comma-delimited list of colon-delimited pairs of SourceId:ActionId. The action IDs
+ * listed by this flag should have their {@code PendingIntent}s overridden with the source's
+ * default intent drawn from Safety Center's config file, if available.
+ */
+ private static String getActionsToOverrideWithDefaultIntent() {
+ return getString(
+ PROPERTY_ACTIONS_TO_OVERRIDE_WITH_DEFAULT_INTENT,
+ sActionsToOverrideWithDefaultIntentDefault);
+ }
+
/** Returns a duration after which a temporarily hidden issue will resurface. */
public static Duration getTemporarilyHiddenIssueResurfaceDelay() {
return getDuration(
@@ -408,19 +433,10 @@ public final class SafetyCenterFlags {
*/
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;
+ List<String> allowlist =
+ getStringListValueFromStringMapping(
+ getIssueCategoryAllowlists(), Integer.toString(issueCategory));
+ return allowlist.isEmpty() || allowlist.contains(safetySourceId);
}
/** Returns a set of package certificates allowlisted for the given package name. */
@@ -452,6 +468,16 @@ public final class SafetyCenterFlags {
}
/**
+ * Returns a list of action IDs that should be overridden with the source's default intent drawn
+ * from the config for a given source.
+ */
+ public static List<String> getActionsToOverrideWithDefaultIntentForSource(
+ String safetySourceId) {
+ return getStringListValueFromStringMapping(
+ getActionsToOverrideWithDefaultIntent(), safetySourceId);
+ }
+
+ /**
* Returns whether to show subpages in the Safety Center UI for Android-U instead of the
* expand-and-collapse list implementation.
*/
@@ -515,15 +541,15 @@ public final class SafetyCenterFlags {
* pairs of integers and longs.
*/
@Nullable
- private static Long getLongValueFromStringMapping(String config, int key) {
- String valueString = getStringValueFromStringMapping(config, key);
+ private static Long getLongValueFromStringMapping(String mapping, int key) {
+ String valueString = getStringValueFromStringMapping(mapping, key);
if (valueString == null) {
return null;
}
try {
return Long.parseLong(valueString);
} catch (NumberFormatException e) {
- Log.w(TAG, "Badly formatted string config: " + config, e);
+ Log.w(TAG, "Badly formatted string mapping: " + mapping, e);
return null;
}
}
@@ -533,8 +559,8 @@ public final class SafetyCenterFlags {
* of integers and strings.
*/
@Nullable
- private static String getStringValueFromStringMapping(String config, int key) {
- return getStringValueFromStringMapping(config, Integer.toString(key));
+ private static String getStringValueFromStringMapping(String mapping, int key) {
+ return getStringValueFromStringMapping(mapping, Integer.toString(key));
}
/**
@@ -542,15 +568,15 @@ public final class SafetyCenterFlags {
* string pairs.
*/
@Nullable
- private static String getStringValueFromStringMapping(String config, String key) {
- if (config.isEmpty()) {
+ private static String getStringValueFromStringMapping(String mapping, String key) {
+ if (mapping.isEmpty()) {
return null;
}
- String[] pairsList = config.split(",");
+ String[] pairsList = mapping.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);
+ Log.w(TAG, "Badly formatted string mapping: " + mapping);
continue;
}
if (pair[0].equals(key)) {
@@ -560,5 +586,14 @@ public final class SafetyCenterFlags {
return null;
}
+ private static List<String> getStringListValueFromStringMapping(String mapping, String key) {
+ String value = getStringValueFromStringMapping(mapping, key);
+ if (value == null) {
+ return Collections.emptyList();
+ }
+
+ return Arrays.asList(value.split("\\|"));
+ }
+
private SafetyCenterFlags() {}
}
diff --git a/service/java/com/android/safetycenter/SafetyCenterService.java b/service/java/com/android/safetycenter/SafetyCenterService.java
index 98e97a26c..62d0a0d78 100644
--- a/service/java/com/android/safetycenter/SafetyCenterService.java
+++ b/service/java/com/android/safetycenter/SafetyCenterService.java
@@ -76,9 +76,9 @@ import com.android.modules.utils.BackgroundThread;
import com.android.modules.utils.build.SdkLevel;
import com.android.permission.util.ForegroundThread;
import com.android.permission.util.UserUtils;
-import com.android.safetycenter.data.AndroidLockScreenFix;
import com.android.safetycenter.data.SafetyCenterDataManager;
import com.android.safetycenter.data.SafetyEventFix;
+import com.android.safetycenter.data.SafetySourceDataFix;
import com.android.safetycenter.internaldata.SafetyCenterIds;
import com.android.safetycenter.internaldata.SafetyCenterIssueActionId;
import com.android.safetycenter.internaldata.SafetyCenterIssueId;
@@ -120,6 +120,8 @@ public final class SafetyCenterService extends SystemService {
@GuardedBy("mApiLock")
private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker;
+ private final SafetySourceDataFix mSafetySourceDataFix;
+
@GuardedBy("mApiLock")
private final SafetyCenterDataManager mSafetyCenterDataManager;
@@ -151,6 +153,10 @@ public final class SafetyCenterService extends SystemService {
mSafetyCenterResourcesApk = new SafetyCenterResourcesApk(context);
mSafetyCenterConfigReader = new SafetyCenterConfigReader(mSafetyCenterResourcesApk);
mSafetyCenterRefreshTracker = new SafetyCenterRefreshTracker(context);
+ PendingIntentFactory pendingIntentFactory =
+ new PendingIntentFactory(context, mSafetyCenterResourcesApk);
+ mSafetySourceDataFix =
+ new SafetySourceDataFix(context, pendingIntentFactory, mSafetyCenterConfigReader);
mSafetyCenterDataManager =
new SafetyCenterDataManager(
context, mSafetyCenterConfigReader, mSafetyCenterRefreshTracker, mApiLock);
@@ -160,7 +166,7 @@ public final class SafetyCenterService extends SystemService {
mSafetyCenterResourcesApk,
mSafetyCenterConfigReader,
mSafetyCenterRefreshTracker,
- new PendingIntentFactory(context, mSafetyCenterResourcesApk),
+ pendingIntentFactory,
mSafetyCenterDataManager);
mSafetyCenterListeners = new SafetyCenterListeners(mSafetyCenterDataFactory);
mNotificationChannels = new SafetyCenterNotificationChannels(mSafetyCenterResourcesApk);
@@ -310,8 +316,8 @@ public final class SafetyCenterService extends SystemService {
UserProfileGroup userProfileGroup = UserProfileGroup.fromUser(getContext(), userId);
synchronized (mApiLock) {
safetySourceData =
- AndroidLockScreenFix.maybeOverrideSafetySourceData(
- getContext(), safetySourceId, safetySourceData);
+ mSafetySourceDataFix.maybeOverrideSafetySourceData(
+ safetySourceId, safetySourceData, packageName, userId);
safetyEvent =
SafetyEventFix.maybeOverrideSafetyEvent(
mSafetyCenterDataManager,
diff --git a/service/java/com/android/safetycenter/data/AndroidLockScreenFix.java b/service/java/com/android/safetycenter/data/AndroidLockScreenFix.java
index e46ba2f4d..53043c0f8 100644
--- a/service/java/com/android/safetycenter/data/AndroidLockScreenFix.java
+++ b/service/java/com/android/safetycenter/data/AndroidLockScreenFix.java
@@ -27,8 +27,6 @@ import android.safetycenter.SafetySourceIssue;
import android.safetycenter.SafetySourceStatus;
import android.util.Log;
-import androidx.annotation.Nullable;
-
import com.android.modules.utils.build.SdkLevel;
import com.android.safetycenter.PendingIntentFactory;
import com.android.safetycenter.SafetyCenterFlags;
@@ -38,10 +36,8 @@ import java.util.List;
/**
* A class to work around an issue with the {@code AndroidLockScreen} safety source, by potentially
* overriding its {@link SafetySourceData}.
- *
- * @hide
*/
-public final class AndroidLockScreenFix {
+final class AndroidLockScreenFix {
private static final String TAG = "AndroidLockScreenFix";
@@ -55,9 +51,22 @@ public final class AndroidLockScreenFix {
private AndroidLockScreenFix() {}
+ static boolean shouldApplyFix(String sourceId) {
+ if (SdkLevel.isAtLeastU()) {
+ // No need to override on U+ as the issue has been fixed in a T QPR release.
+ // As such, U+ fields for the SafetySourceData are not taken into account in the methods
+ // below.
+ return false;
+ }
+ if (!ANDROID_LOCK_SCREEN_SOURCE_ID.equals(sourceId)) {
+ return false;
+ }
+ return SafetyCenterFlags.getReplaceLockScreenIconAction();
+ }
+
/**
- * Potentially overrides the {@link SafetySourceData} of the {@code AndroidLockScreen} source by
- * replacing its {@link PendingIntent}s.
+ * Overrides the {@link SafetySourceData} of the {@code AndroidLockScreen} source by replacing
+ * its {@link PendingIntent}s.
*
* <p>This is done because of a bug in the Settings app where the {@link PendingIntent}s created
* end up referencing either the {@link SafetyCenterEntry#getPendingIntent()} or the {@link
@@ -70,67 +79,45 @@ public final class AndroidLockScreenFix {
* different request codes for the different {@link PendingIntent}s to ensure new instances are
* created (the key does take into account the request code).
*/
- @Nullable
- public static SafetySourceData maybeOverrideSafetySourceData(
- Context context, String sourceId, @Nullable SafetySourceData safetySourceData) {
- if (safetySourceData == null) {
- return null;
- }
- if (SdkLevel.isAtLeastU()) {
- // No need to override on U+ as the issue has been fixed in a T QPR release.
- // As such, U+ fields for the SafetySourceData are not taken into account in the methods
- // below.
- return safetySourceData;
- }
- if (!ANDROID_LOCK_SCREEN_SOURCE_ID.equals(sourceId)) {
- return safetySourceData;
- }
- if (!SafetyCenterFlags.getReplaceLockScreenIconAction()) {
- return safetySourceData;
- }
- return overrideTiramisuSafetySourceData(context, safetySourceData);
- }
+ static SafetySourceData applyFix(Context context, SafetySourceData data) {
+ SafetySourceData.Builder overriddenData =
+ SafetySourceDataOverrides.copyDataToBuilderWithoutIssues(data);
- private static SafetySourceData overrideTiramisuSafetySourceData(
- Context context, SafetySourceData safetySourceData) {
- SafetySourceData.Builder overriddenSafetySourceData = new SafetySourceData.Builder();
- SafetySourceStatus safetySourceStatus = safetySourceData.getStatus();
- if (safetySourceStatus != null) {
- overriddenSafetySourceData.setStatus(
- overrideTiramisuSafetySourceStatus(context, safetySourceStatus));
+ SafetySourceStatus originalStatus = data.getStatus();
+ if (originalStatus != null) {
+ overriddenData.setStatus(overrideTiramisuSafetySourceStatus(context, originalStatus));
}
- List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues();
- for (int i = 0; i < safetySourceIssues.size(); i++) {
- SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
- overriddenSafetySourceData.addIssue(
- overrideTiramisuSafetySourceIssue(context, safetySourceIssue));
+
+ List<SafetySourceIssue> issues = data.getIssues();
+ for (int i = 0; i < issues.size(); i++) {
+ overriddenData.addIssue(overrideTiramisuIssue(context, issues.get(i)));
}
- return overriddenSafetySourceData.build();
+
+ return overriddenData.build();
}
private static SafetySourceStatus overrideTiramisuSafetySourceStatus(
- Context context, SafetySourceStatus safetySourceStatus) {
- SafetySourceStatus.Builder overriddenSafetySourceStatus =
- new SafetySourceStatus.Builder(
- safetySourceStatus.getTitle(),
- safetySourceStatus.getSummary(),
- safetySourceStatus.getSeverityLevel())
- .setPendingIntent(
- overridePendingIntent(
- context,
- safetySourceStatus.getPendingIntent(),
- /* isIconAction= */ false))
- .setEnabled(safetySourceStatus.isEnabled());
- SafetySourceStatus.IconAction iconAction = safetySourceStatus.getIconAction();
+ Context context, SafetySourceStatus status) {
+ SafetySourceStatus.Builder overriddenStatus =
+ SafetySourceDataOverrides.copyStatusToBuilder(status);
+
+ PendingIntent originalPendingIntent = status.getPendingIntent();
+ if (originalPendingIntent != null) {
+ overriddenStatus.setPendingIntent(
+ overridePendingIntent(
+ context, originalPendingIntent, /* isIconAction= */ false));
+ }
+
+ SafetySourceStatus.IconAction iconAction = status.getIconAction();
if (iconAction != null) {
- overriddenSafetySourceStatus.setIconAction(
- overrideTiramisuSafetySourceStatusIconAction(
- context, safetySourceStatus.getIconAction()));
+ overriddenStatus.setIconAction(
+ overrideTiramisuIconAction(context, status.getIconAction()));
}
- return overriddenSafetySourceStatus.build();
+
+ return overriddenStatus.build();
}
- private static SafetySourceStatus.IconAction overrideTiramisuSafetySourceStatusIconAction(
+ private static SafetySourceStatus.IconAction overrideTiramisuIconAction(
Context context, SafetySourceStatus.IconAction iconAction) {
return new SafetySourceStatus.IconAction(
iconAction.getIconType(),
@@ -138,45 +125,30 @@ public final class AndroidLockScreenFix {
context, iconAction.getPendingIntent(), /* isIconAction= */ true));
}
- private static SafetySourceIssue overrideTiramisuSafetySourceIssue(
- Context context, SafetySourceIssue safetySourceIssue) {
- SafetySourceIssue.Builder overriddenSafetySourceIssue =
- new SafetySourceIssue.Builder(
- safetySourceIssue.getId(),
- safetySourceIssue.getTitle(),
- safetySourceIssue.getSummary(),
- safetySourceIssue.getSeverityLevel(),
- safetySourceIssue.getIssueTypeId())
- .setSubtitle(safetySourceIssue.getSubtitle())
- .setIssueCategory(safetySourceIssue.getIssueCategory())
- .setOnDismissPendingIntent(safetySourceIssue.getOnDismissPendingIntent());
- List<SafetySourceIssue.Action> actions = safetySourceIssue.getActions();
+ private static SafetySourceIssue overrideTiramisuIssue(
+ Context context, SafetySourceIssue issue) {
+ SafetySourceIssue.Builder overriddenIssue =
+ SafetySourceDataOverrides.copyIssueToBuilderWithoutActions(issue);
+
+ List<SafetySourceIssue.Action> actions = issue.getActions();
for (int i = 0; i < actions.size(); i++) {
SafetySourceIssue.Action action = actions.get(i);
- overriddenSafetySourceIssue.addAction(
- overrideTiramisuSafetySourceIssueAction(context, action));
+ overriddenIssue.addAction(overrideTiramisuIssueAction(context, action));
}
- return overriddenSafetySourceIssue.build();
+
+ return overriddenIssue.build();
}
- private static SafetySourceIssue.Action overrideTiramisuSafetySourceIssueAction(
+ private static SafetySourceIssue.Action overrideTiramisuIssueAction(
Context context, SafetySourceIssue.Action action) {
- return new SafetySourceIssue.Action.Builder(
- action.getId(),
- action.getLabel(),
- overridePendingIntent(
- context, action.getPendingIntent(), /* isIconAction= */ false))
- .setWillResolve(action.willResolve())
- .setSuccessMessage(action.getSuccessMessage())
- .build();
+ PendingIntent pendingIntent =
+ overridePendingIntent(
+ context, action.getPendingIntent(), /* isIconAction= */ false);
+ return SafetySourceDataOverrides.overrideActionPendingIntent(action, pendingIntent);
}
- @Nullable
private static PendingIntent overridePendingIntent(
- Context context, @Nullable PendingIntent pendingIntent, boolean isIconAction) {
- if (pendingIntent == null) {
- return null;
- }
+ Context context, PendingIntent pendingIntent, boolean isIconAction) {
String settingsPackageName = pendingIntent.getCreatorPackage();
int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
Context settingsPackageContext =
diff --git a/service/java/com/android/safetycenter/data/DefaultActionOverrideFix.java b/service/java/com/android/safetycenter/data/DefaultActionOverrideFix.java
new file mode 100644
index 000000000..9ca188670
--- /dev/null
+++ b/service/java/com/android/safetycenter/data/DefaultActionOverrideFix.java
@@ -0,0 +1,180 @@
+/*
+ * 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.UPSIDE_DOWN_CAKE;
+
+import android.annotation.UserIdInt;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.safetycenter.SafetySourceData;
+import android.safetycenter.SafetySourceIssue;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.permission.util.UserUtils;
+import com.android.safetycenter.PendingIntentFactory;
+import com.android.safetycenter.SafetyCenterConfigReader;
+import com.android.safetycenter.SafetyCenterFlags;
+
+import java.util.List;
+
+/**
+ * Replaces {@link SafetySourceIssue.Action}s with the corresponding source's default intent drawn
+ * from the Safety Center config.
+ *
+ * <p>Actions to be replaced are controlled by the {@code
+ * safety_center_actions_to_override_with_default_intent} DeviceConfig flag.
+ *
+ * <p>This is done to support cases where we allow OEMs to override intents in the config, but
+ * sources are unaware of and unable to access those overrides when providing issues and
+ * notifications. We use the default intent when sources provide a null pending intent in their
+ * status. This fix allows us to implement a similar behavior for actions, without changing the
+ * non-null requirement on their pending intent fields.
+ */
+final class DefaultActionOverrideFix {
+
+ private final Context mContext;
+ private final PendingIntentFactory mPendingIntentFactory;
+ private final SafetyCenterConfigReader mSafetyCenterConfigReader;
+
+ DefaultActionOverrideFix(
+ Context context,
+ PendingIntentFactory pendingIntentFactory,
+ SafetyCenterConfigReader safetyCenterConfigReader) {
+ mContext = context;
+ mPendingIntentFactory = pendingIntentFactory;
+ mSafetyCenterConfigReader = safetyCenterConfigReader;
+ }
+
+ static boolean shouldApplyFix(String sourceId) {
+ List<String> actionsToOverride =
+ SafetyCenterFlags.getActionsToOverrideWithDefaultIntentForSource(sourceId);
+ return !actionsToOverride.isEmpty();
+ }
+
+ SafetySourceData applyFix(
+ String sourceId,
+ SafetySourceData safetySourceData,
+ String packageName,
+ @UserIdInt int userId) {
+ if (safetySourceData.getIssues().isEmpty()) {
+ return safetySourceData;
+ }
+
+ PendingIntent defaultIntentForSource =
+ getDefaultIntentForSource(sourceId, packageName, userId);
+ if (defaultIntentForSource == null) {
+ // If there's no default intent, we can't override any actions with it.
+ return safetySourceData;
+ }
+
+ List<String> actionsToOverride =
+ SafetyCenterFlags.getActionsToOverrideWithDefaultIntentForSource(sourceId);
+ if (actionsToOverride.isEmpty()) {
+ // This shouldn't happen if shouldApplyFix is called first, but we check for good
+ // measure.
+ return safetySourceData;
+ }
+
+ SafetySourceData.Builder overriddenSafetySourceData =
+ SafetySourceDataOverrides.copyDataToBuilderWithoutIssues(safetySourceData);
+ List<SafetySourceIssue> issues = safetySourceData.getIssues();
+ for (int i = 0; i < issues.size(); i++) {
+ overriddenSafetySourceData.addIssue(
+ maybeOverrideActionsWithDefaultIntent(
+ issues.get(i), actionsToOverride, defaultIntentForSource));
+ }
+
+ return overriddenSafetySourceData.build();
+ }
+
+ @Nullable
+ private PendingIntent getDefaultIntentForSource(
+ String sourceId, String packageName, @UserIdInt int userId) {
+ SafetyCenterConfigReader.ExternalSafetySource externalSafetySource =
+ mSafetyCenterConfigReader.getExternalSafetySource(sourceId, packageName);
+ if (externalSafetySource == null) {
+ return null;
+ }
+
+ boolean isQuietModeEnabled =
+ UserUtils.isManagedProfile(userId, mContext)
+ && !UserUtils.isProfileRunning(userId, mContext);
+
+ return mPendingIntentFactory.getPendingIntent(
+ sourceId,
+ externalSafetySource.getSafetySource().getIntentAction(),
+ packageName,
+ userId,
+ isQuietModeEnabled);
+ }
+
+ private SafetySourceIssue maybeOverrideActionsWithDefaultIntent(
+ SafetySourceIssue issue, List<String> actionsToOverride, PendingIntent defaultIntent) {
+ SafetySourceIssue.Builder overriddenIssue =
+ SafetySourceDataOverrides.copyIssueToBuilderWithoutActions(issue);
+
+ List<SafetySourceIssue.Action> actions = issue.getActions();
+ for (int i = 0; i < actions.size(); i++) {
+ overriddenIssue.addAction(
+ maybeOverrideAction(actions.get(i), actionsToOverride, defaultIntent));
+ }
+
+ if (SdkLevel.isAtLeastU()) {
+ overriddenIssue.setCustomNotification(
+ maybeOverrideNotification(
+ issue.getCustomNotification(), actionsToOverride, defaultIntent));
+ }
+
+ return overriddenIssue.build();
+ }
+
+ @RequiresApi(UPSIDE_DOWN_CAKE)
+ @Nullable
+ private static SafetySourceIssue.Notification maybeOverrideNotification(
+ @Nullable SafetySourceIssue.Notification notification,
+ List<String> actionsToOverride,
+ PendingIntent defaultIntent) {
+ if (notification == null) {
+ return null;
+ }
+
+ SafetySourceIssue.Notification.Builder overriddenNotification =
+ new SafetySourceIssue.Notification.Builder(notification).clearActions();
+
+ List<SafetySourceIssue.Action> actions = notification.getActions();
+ for (int i = 0; i < actions.size(); i++) {
+ overriddenNotification.addAction(
+ maybeOverrideAction(actions.get(i), actionsToOverride, defaultIntent));
+ }
+
+ return overriddenNotification.build();
+ }
+
+ private static SafetySourceIssue.Action maybeOverrideAction(
+ SafetySourceIssue.Action action,
+ List<String> actionsToOverride,
+ PendingIntent defaultIntent) {
+ if (actionsToOverride.contains(action.getId())) {
+ return SafetySourceDataOverrides.overrideActionPendingIntent(action, defaultIntent);
+ }
+ return action;
+ }
+}
diff --git a/service/java/com/android/safetycenter/data/SafetySourceDataFix.java b/service/java/com/android/safetycenter/data/SafetySourceDataFix.java
new file mode 100644
index 000000000..a34f3b03b
--- /dev/null
+++ b/service/java/com/android/safetycenter/data/SafetySourceDataFix.java
@@ -0,0 +1,76 @@
+/*
+ * 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 android.annotation.UserIdInt;
+import android.content.Context;
+import android.safetycenter.SafetySourceData;
+
+import androidx.annotation.Nullable;
+
+import com.android.safetycenter.PendingIntentFactory;
+import com.android.safetycenter.SafetyCenterConfigReader;
+
+/**
+ * Applies various workarounds and fixes to {@link SafetySourceData} as it's received.
+ *
+ * @hide
+ */
+public final class SafetySourceDataFix {
+
+ private final DefaultActionOverrideFix mDefaultActionOverrideFix;
+ private Context mContext;
+
+ public SafetySourceDataFix(
+ Context context,
+ PendingIntentFactory pendingIntentFactory,
+ SafetyCenterConfigReader safetyCenterConfigReader) {
+ mContext = context;
+ mDefaultActionOverrideFix =
+ new DefaultActionOverrideFix(
+ context, pendingIntentFactory, safetyCenterConfigReader);
+ }
+
+ /**
+ * Potentially overrides the {@link SafetySourceData}.
+ *
+ * <p>Should be called when the data is received from a source and before it's stored by Safety
+ * Center.
+ */
+ @Nullable
+ public SafetySourceData maybeOverrideSafetySourceData(
+ String sourceId,
+ @Nullable SafetySourceData safetySourceData,
+ String packageName,
+ @UserIdInt int userId) {
+ if (safetySourceData == null) {
+ return null;
+ }
+
+ if (AndroidLockScreenFix.shouldApplyFix(sourceId)) {
+ safetySourceData = AndroidLockScreenFix.applyFix(mContext, safetySourceData);
+ }
+
+ if (DefaultActionOverrideFix.shouldApplyFix(sourceId)) {
+ safetySourceData =
+ mDefaultActionOverrideFix.applyFix(
+ sourceId, safetySourceData, packageName, userId);
+ }
+
+ return safetySourceData;
+ }
+}
diff --git a/service/java/com/android/safetycenter/data/SafetySourceDataOverrides.java b/service/java/com/android/safetycenter/data/SafetySourceDataOverrides.java
new file mode 100644
index 000000000..b292ae6cb
--- /dev/null
+++ b/service/java/com/android/safetycenter/data/SafetySourceDataOverrides.java
@@ -0,0 +1,81 @@
+/*
+ * 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 android.app.PendingIntent;
+import android.safetycenter.SafetySourceData;
+import android.safetycenter.SafetySourceIssue;
+import android.safetycenter.SafetySourceStatus;
+
+import com.android.modules.utils.build.SdkLevel;
+
+final class SafetySourceDataOverrides {
+ private SafetySourceDataOverrides() {}
+
+ static SafetySourceData.Builder copyDataToBuilderWithoutIssues(SafetySourceData data) {
+ if (SdkLevel.isAtLeastU()) {
+ return new SafetySourceData.Builder(data).clearIssues();
+ }
+
+ // Copy T-only fields
+ return new SafetySourceData.Builder().setStatus(data.getStatus());
+ }
+
+ static SafetySourceStatus.Builder copyStatusToBuilder(SafetySourceStatus status) {
+ if (SdkLevel.isAtLeastU()) {
+ return new SafetySourceStatus.Builder(status);
+ }
+
+ // Copy T-only fields
+ return new SafetySourceStatus.Builder(
+ status.getTitle(), status.getSummary(), status.getSeverityLevel())
+ .setPendingIntent(status.getPendingIntent())
+ .setEnabled(status.isEnabled())
+ .setIconAction(status.getIconAction());
+ }
+
+ static SafetySourceIssue.Builder copyIssueToBuilderWithoutActions(SafetySourceIssue issue) {
+ if (SdkLevel.isAtLeastU()) {
+ return new SafetySourceIssue.Builder(issue).clearActions();
+ }
+
+ // Copy T-only fields
+ return new SafetySourceIssue.Builder(
+ issue.getId(),
+ issue.getTitle(),
+ issue.getSummary(),
+ issue.getSeverityLevel(),
+ issue.getIssueTypeId())
+ .setIssueCategory(issue.getIssueCategory())
+ .setSubtitle(issue.getSubtitle())
+ .setOnDismissPendingIntent(issue.getOnDismissPendingIntent());
+ }
+
+ /**
+ * Returns an new {@link SafetySourceIssue.Action} object, replacing its {@link PendingIntent}
+ * with the one supplied.
+ */
+ static SafetySourceIssue.Action overrideActionPendingIntent(
+ SafetySourceIssue.Action action, PendingIntent pendingIntent) {
+ // TODO(b/303443020): Add setter for pendingIntent so this method can use the copy builder.
+ return new SafetySourceIssue.Action.Builder(
+ action.getId(), action.getLabel(), pendingIntent)
+ .setWillResolve(action.willResolve())
+ .setSuccessMessage(action.getSuccessMessage())
+ .build();
+ }
+}
diff --git a/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt
index 19922e4f7..d90fbcd1e 100644
--- a/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt
+++ b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetyCenterManagerTest.kt
@@ -61,7 +61,6 @@ import android.safetycenter.config.SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
-import com.android.compatibility.common.preconditions.ScreenLockHelper
import com.android.compatibility.common.util.SystemUtil
import com.android.modules.utils.build.SdkLevel
import com.android.safetycenter.internaldata.SafetyCenterBundles
@@ -137,7 +136,6 @@ import java.time.Duration
import kotlin.test.assertFailsWith
import kotlinx.coroutines.TimeoutCancellationException
import org.junit.Assume.assumeFalse
-import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -3734,46 +3732,6 @@ class SafetyCenterManagerTest {
}
@Test
- fun lockScreenSource_withoutReplaceLockScreenIconActionFlag_doesntReplace() {
- // Must have a screen lock for the icon action to be set
- assumeTrue(ScreenLockHelper.isDeviceSecure(context))
- safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.settingsLockScreenSourceConfig)
- val listener = safetyCenterTestHelper.addListener()
- SafetyCenterFlags.replaceLockScreenIconAction = false
-
- safetyCenterManager.refreshSafetySourcesWithPermission(REFRESH_REASON_PAGE_OPEN)
- // Skip loading data.
- listener.receiveSafetyCenterData()
-
- val lockScreenSafetyCenterData = listener.receiveSafetyCenterData()
- val lockScreenEntry = lockScreenSafetyCenterData.entriesOrGroups.first().entry!!
- val entryPendingIntent = lockScreenEntry.pendingIntent!!
- val iconActionPendingIntent = lockScreenEntry.iconAction!!.pendingIntent
- // This test passes for now but will eventually start failing once we introduce the fix in
- // the Settings app. This will warn if the assumption is failed rather than fail, at which
- // point we can remove this test (and potentially even this magnificent hack).
- assumeTrue(iconActionPendingIntent == entryPendingIntent)
- }
-
- @Test
- fun lockScreenSource_withReplaceLockScreenIconActionFlag_replaces() {
- // Must have a screen lock for the icon action to be set
- assumeTrue(ScreenLockHelper.isDeviceSecure(context))
- safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.settingsLockScreenSourceConfig)
- val listener = safetyCenterTestHelper.addListener()
-
- safetyCenterManager.refreshSafetySourcesWithPermission(REFRESH_REASON_PAGE_OPEN)
- // Skip loading data.
- listener.receiveSafetyCenterData()
-
- val lockScreenSafetyCenterData = listener.receiveSafetyCenterData()
- val lockScreenEntry = lockScreenSafetyCenterData.entriesOrGroups.first().entry!!
- val entryPendingIntent = lockScreenEntry.pendingIntent!!
- val iconActionPendingIntent = lockScreenEntry.iconAction!!.pendingIntent
- assertThat(iconActionPendingIntent).isNotEqualTo(entryPendingIntent)
- }
-
- @Test
fun beforeAnyDataSet_noLastUpdatedTimestamps() {
safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceConfig)
diff --git a/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetySourceDataFixesTest.kt b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetySourceDataFixesTest.kt
new file mode 100644
index 000000000..4ba293eb9
--- /dev/null
+++ b/tests/functional/safetycenter/singleuser/src/android/safetycenter/functional/SafetySourceDataFixesTest.kt
@@ -0,0 +1,298 @@
+package android.safetycenter.functional
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+import android.safetycenter.SafetyCenterManager
+import android.safetycenter.SafetySourceData
+import android.safetycenter.SafetySourceData.SEVERITY_LEVEL_INFORMATION
+import android.safetycenter.SafetySourceIssue
+import android.safetycenter.SafetySourceStatus
+import androidx.annotation.RequiresApi
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import com.android.compatibility.common.preconditions.ScreenLockHelper
+import com.android.safetycenter.testing.SafetyCenterApisWithShellPermissions.getSafetySourceDataWithPermission
+import com.android.safetycenter.testing.SafetyCenterApisWithShellPermissions.refreshSafetySourcesWithPermission
+import com.android.safetycenter.testing.SafetyCenterFlags
+import com.android.safetycenter.testing.SafetyCenterTestConfigs
+import com.android.safetycenter.testing.SafetyCenterTestConfigs.Companion.SINGLE_SOURCE_ID
+import com.android.safetycenter.testing.SafetyCenterTestConfigs.Companion.SOURCE_ID_1
+import com.android.safetycenter.testing.SafetyCenterTestConfigs.Companion.SOURCE_ID_2
+import com.android.safetycenter.testing.SafetyCenterTestHelper
+import com.android.safetycenter.testing.SafetyCenterTestRule
+import com.android.safetycenter.testing.SafetySourceTestData
+import com.android.safetycenter.testing.ShellPermissions.callWithShellPermissionIdentity
+import com.android.safetycenter.testing.SupportsSafetyCenterRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Functional tests for "fixes" applied to Safety Source data received by [SafetyCenterManager]. */
+@RunWith(AndroidJUnit4::class)
+class SafetySourceDataFixesTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val safetyCenterTestHelper = SafetyCenterTestHelper(context)
+ private val safetySourceTestData = SafetySourceTestData(context)
+ private val safetyCenterTestConfigs = SafetyCenterTestConfigs(context)
+ private val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)!!
+
+ @get:Rule(order = 1) val supportsSafetyCenterRule = SupportsSafetyCenterRule(context)
+ @get:Rule(order = 2) val safetyCenterTestRule = SafetyCenterTestRule(safetyCenterTestHelper)
+
+ @Test
+ fun lockScreenSource_withoutReplaceLockScreenIconActionFlag_doesntReplace() {
+ // Must have a screen lock for the icon action to be set
+ assumeTrue(ScreenLockHelper.isDeviceSecure(context))
+ safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.settingsLockScreenSourceConfig)
+ val listener = safetyCenterTestHelper.addListener()
+ SafetyCenterFlags.replaceLockScreenIconAction = false
+
+ safetyCenterManager.refreshSafetySourcesWithPermission(
+ SafetyCenterManager.REFRESH_REASON_PAGE_OPEN
+ )
+ // Skip loading data.
+ listener.receiveSafetyCenterData()
+
+ val lockScreenSafetyCenterData = listener.receiveSafetyCenterData()
+ val lockScreenEntry = lockScreenSafetyCenterData.entriesOrGroups.first().entry!!
+ val entryPendingIntent = lockScreenEntry.pendingIntent!!
+ val iconActionPendingIntent = lockScreenEntry.iconAction!!.pendingIntent
+ // This test passes for now but will eventually start failing once we introduce the fix in
+ // the Settings app. This will warn if the assumption is failed rather than fail, at which
+ // point we can remove this test (and potentially even this magnificent hack).
+ assumeTrue(iconActionPendingIntent == entryPendingIntent)
+ }
+
+ @Test
+ fun lockScreenSource_withReplaceLockScreenIconActionFlag_replaces() {
+ // Must have a screen lock for the icon action to be set
+ assumeTrue(ScreenLockHelper.isDeviceSecure(context))
+ safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.settingsLockScreenSourceConfig)
+ val listener = safetyCenterTestHelper.addListener()
+
+ safetyCenterManager.refreshSafetySourcesWithPermission(
+ SafetyCenterManager.REFRESH_REASON_PAGE_OPEN
+ )
+ // Skip loading data.
+ listener.receiveSafetyCenterData()
+
+ val lockScreenSafetyCenterData = listener.receiveSafetyCenterData()
+ val lockScreenEntry = lockScreenSafetyCenterData.entriesOrGroups.first().entry!!
+ val entryPendingIntent = lockScreenEntry.pendingIntent!!
+ val iconActionPendingIntent = lockScreenEntry.iconAction!!.pendingIntent
+ assertThat(iconActionPendingIntent).isNotEqualTo(entryPendingIntent)
+ }
+
+ @Test
+ fun defaultActionOverride_issue_overridesMatchingActions() {
+ safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceConfig)
+ val targetActionId = "TargetActionId"
+ SafetyCenterFlags.actionsToOverrideWithDefaultIntent =
+ mapOf(SINGLE_SOURCE_ID to setOf(targetActionId, "AdditionalActionId"))
+
+ val originalPendingIntent = pendingIntent(Intent("blah.wrong.INTENT"))
+ val dataWithActionToOverride =
+ sourceDataBuilder()
+ .addIssue(
+ issueBuilder()
+ .clearActions()
+ .addAction(
+ safetySourceTestData.action(
+ id = targetActionId,
+ pendingIntent = originalPendingIntent
+ )
+ )
+ .build()
+ )
+ .build()
+
+ safetyCenterTestHelper.setData(SINGLE_SOURCE_ID, dataWithActionToOverride)
+
+ val overriddenPendingIntent =
+ safetyCenterManager
+ .getSafetySourceDataWithPermission(SINGLE_SOURCE_ID)!!
+ .issues[0]
+ .actions[0]
+ .pendingIntent
+ val expectedPendingIntent =
+ pendingIntent(
+ Intent(SafetyCenterTestConfigs.ACTION_TEST_ACTIVITY).setPackage(context.packageName)
+ )
+ assertThat(intentsFilterEqual(overriddenPendingIntent, expectedPendingIntent)).isTrue()
+ }
+ @Test
+ @SdkSuppress(minSdkVersion = UPSIDE_DOWN_CAKE)
+ fun defaultActionOverride_notification_overridesMatchingActions() {
+ safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceConfig)
+ val targetActionId = "TargetActionId"
+ SafetyCenterFlags.actionsToOverrideWithDefaultIntent =
+ mapOf(SINGLE_SOURCE_ID to setOf(targetActionId, "AdditionalActionId"))
+
+ val originalPendingIntent = pendingIntent(Intent("blah.wrong.INTENT"))
+ val dataWithNotificationActionToOverride =
+ sourceDataBuilder()
+ .addIssue(
+ issueBuilder()
+ .setCustomNotification(
+ notification(
+ safetySourceTestData.action(
+ id = targetActionId,
+ pendingIntent = originalPendingIntent
+ )
+ )
+ )
+ .build()
+ )
+ .build()
+
+ safetyCenterTestHelper.setData(SINGLE_SOURCE_ID, dataWithNotificationActionToOverride)
+
+ val overriddenPendingIntent =
+ safetyCenterManager
+ .getSafetySourceDataWithPermission(SINGLE_SOURCE_ID)!!
+ .issues[0]
+ .customNotification!!
+ .actions[0]
+ .pendingIntent
+ val expectedPendingIntent =
+ pendingIntent(
+ Intent(SafetyCenterTestConfigs.ACTION_TEST_ACTIVITY).setPackage(context.packageName)
+ )
+ assertThat(intentsFilterEqual(overriddenPendingIntent, expectedPendingIntent)).isTrue()
+ }
+
+ @Test
+ fun defaultActionOverride_sameActionIdDifferentSource_doesNotOverride() {
+ safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig)
+ val targetActionId = "TargetActionId"
+ SafetyCenterFlags.actionsToOverrideWithDefaultIntent =
+ mapOf(SOURCE_ID_1 to setOf(targetActionId, "AdditionalActionId"))
+
+ val originalPendingIntent = pendingIntent(Intent("blah.wrong.INTENT"))
+ val dataWithoutActionToOverride =
+ sourceDataBuilder()
+ .addIssue(
+ issueBuilder()
+ .clearActions()
+ .addAction(
+ safetySourceTestData.action(
+ id = targetActionId,
+ pendingIntent = originalPendingIntent
+ )
+ )
+ .build()
+ )
+ .build()
+
+ safetyCenterTestHelper.setData(
+ SOURCE_ID_2, // Different source ID
+ dataWithoutActionToOverride
+ )
+
+ val actualPendingIntent =
+ safetyCenterManager
+ .getSafetySourceDataWithPermission(SOURCE_ID_2)!!
+ .issues[0]
+ .actions[0]
+ .pendingIntent
+ assertThat(intentsFilterEqual(actualPendingIntent, originalPendingIntent)).isTrue()
+ }
+
+ @Test
+ fun defaultActionOverride_sameSourceDifferentActionId_doesNotOverride() {
+ safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.multipleSourcesConfig)
+ SafetyCenterFlags.actionsToOverrideWithDefaultIntent =
+ mapOf(SOURCE_ID_1 to setOf("TargetActionId"))
+
+ val originalPendingIntent = pendingIntent(Intent("blah.wrong.INTENT"))
+ val dataWithoutActionToOverride =
+ sourceDataBuilder()
+ .addIssue(
+ issueBuilder()
+ .clearActions()
+ .addAction(
+ safetySourceTestData.action(
+ id = "DifferentActionId",
+ pendingIntent = originalPendingIntent
+ )
+ )
+ .build()
+ )
+ .build()
+
+ safetyCenterTestHelper.setData(SOURCE_ID_1, dataWithoutActionToOverride)
+
+ val actualPendingIntent =
+ safetyCenterManager
+ .getSafetySourceDataWithPermission(SOURCE_ID_1)!!
+ .issues[0]
+ .actions[0]
+ .pendingIntent
+ assertThat(intentsFilterEqual(actualPendingIntent, originalPendingIntent)).isTrue()
+ }
+
+ @Test
+ fun defaultActionOverride_noDefaultIntent_doesNotOverride() {
+ safetyCenterTestHelper.setConfig(safetyCenterTestConfigs.singleSourceInvalidIntentConfig)
+ val targetActionId = "TargetActionId"
+ SafetyCenterFlags.actionsToOverrideWithDefaultIntent =
+ mapOf(SINGLE_SOURCE_ID to setOf(targetActionId, "AdditionalActionId"))
+
+ val originalPendingIntent = pendingIntent(Intent("blah.wrong.INTENT"))
+ val dataWithActionToOverride =
+ sourceDataBuilder()
+ .addIssue(
+ issueBuilder()
+ .clearActions()
+ .addAction(
+ safetySourceTestData.action(
+ id = targetActionId,
+ pendingIntent = originalPendingIntent
+ )
+ )
+ .build()
+ )
+ .build()
+
+ safetyCenterTestHelper.setData(SINGLE_SOURCE_ID, dataWithActionToOverride)
+
+ val actualPendingIntent =
+ safetyCenterManager
+ .getSafetySourceDataWithPermission(SINGLE_SOURCE_ID)!!
+ .issues[0]
+ .actions[0]
+ .pendingIntent
+ assertThat(intentsFilterEqual(actualPendingIntent, originalPendingIntent)).isTrue()
+ }
+
+ private fun issueBuilder() = safetySourceTestData.defaultInformationIssueBuilder()
+
+ private fun pendingIntent(intent: Intent) =
+ PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+
+ companion object {
+ private fun sourceDataBuilder() =
+ SafetySourceData.Builder()
+ .setStatus(
+ SafetySourceStatus.Builder("OK", "Blah", SEVERITY_LEVEL_INFORMATION).build()
+ )
+
+ @RequiresApi(UPSIDE_DOWN_CAKE)
+ private fun notification(action: SafetySourceIssue.Action) =
+ SafetySourceIssue.Notification.Builder("Blah", "Bleh").addAction(action).build()
+
+ private fun intentsFilterEqual(
+ actualPendingIntent: PendingIntent,
+ expectedPendingIntent: PendingIntent?
+ ) =
+ callWithShellPermissionIdentity("android.permission.GET_INTENT_SENDER_INTENT") {
+ actualPendingIntent.intentFilterEquals(expectedPendingIntent)
+ }
+ }
+}
diff --git a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt
index 714565d62..912ea44ad 100644
--- a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt
+++ b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetyCenterFlags.kt
@@ -203,6 +203,18 @@ object SafetyCenterFlags {
)
/**
+ * Flag containing a map (a comma separated list of colon separated pairs) where the key is a
+ * Safety Source ID and the value is a vertical-bar-delimited list of Action IDs that should
+ * have their PendingIntent replaced with the source's default PendingIntent.
+ */
+ private val actionsToOverrideWithDefaultIntentFlag =
+ Flag(
+ "safety_center_actions_to_override_with_default_intent",
+ defaultValue = emptyMap(),
+ MapParser(StringParser(), SetParser(StringParser(), delimiter = "|"))
+ )
+
+ /**
* Flag that represents a comma delimited list of 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.
@@ -303,6 +315,7 @@ object SafetyCenterFlags {
resurfaceIssueMaxCountsFlag,
resurfaceIssueDelaysFlag,
issueCategoryAllowlistsFlag,
+ actionsToOverrideWithDefaultIntentFlag,
allowedAdditionalPackageCertsFlag,
backgroundRefreshDeniedSourcesFlag,
allowStatsdLoggingFlag,
@@ -358,6 +371,10 @@ object SafetyCenterFlags {
/** A property that allows getting and setting the [issueCategoryAllowlistsFlag]. */
var issueCategoryAllowlists: Map<Int, Set<String>> by issueCategoryAllowlistsFlag
+ /** A property that allows getting and setting the [actionsToOverrideWithDefaultIntentFlag]. */
+ var actionsToOverrideWithDefaultIntent: Map<String, Set<String>> by
+ actionsToOverrideWithDefaultIntentFlag
+
var allowedAdditionalPackageCerts: Map<String, Set<String>> by allowedAdditionalPackageCertsFlag
/** A property that allows getting and setting the [backgroundRefreshDeniedSourcesFlag]. */
diff --git a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetySourceTestData.kt b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetySourceTestData.kt
index 0b2a6c840..2c4f856bb 100644
--- a/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetySourceTestData.kt
+++ b/tests/utils/safetycenter/java/com/android/safetycenter/testing/SafetySourceTestData.kt
@@ -114,14 +114,14 @@ class SafetySourceTestData(private val context: Context) {
summary: String = "Information issue summary"
) =
SafetySourceIssue.Builder(id, title, summary, SEVERITY_LEVEL_INFORMATION, ISSUE_TYPE_ID)
- .addAction(
- Action.Builder(
- INFORMATION_ISSUE_ACTION_ID,
- "Review",
- createTestActivityRedirectPendingIntent()
- )
- .build()
- )
+ .addAction(action())
+
+ /** Creates an action with some defaults set. */
+ fun action(
+ id: String = INFORMATION_ISSUE_ACTION_ID,
+ label: String = "Review",
+ pendingIntent: PendingIntent = createTestActivityRedirectPendingIntent()
+ ) = Action.Builder(id, label, pendingIntent).build()
/**
* A [SafetySourceIssue] with a [SEVERITY_LEVEL_INFORMATION] and a redirecting [Action]. With
@@ -136,14 +136,7 @@ class SafetySourceTestData(private val context: Context) {
ISSUE_TYPE_ID
)
.setSubtitle("Information issue subtitle")
- .addAction(
- Action.Builder(
- INFORMATION_ISSUE_ACTION_ID,
- "Review",
- createTestActivityRedirectPendingIntent()
- )
- .build()
- )
+ .addAction(action())
.build()
/**