summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PermissionController/AndroidManifest.xml7
-rw-r--r--PermissionController/res/layout-v33/preference_issue_card.xml2
-rw-r--r--PermissionController/res/values-v33/dimens.xml1
-rw-r--r--PermissionController/res/values/strings.xml9
-rw-r--r--PermissionController/src/com/android/permissioncontroller/Constants.java15
-rw-r--r--PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt181
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/service/LocationAccessCheck.java159
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java20
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/SafetyCenterQsActivity.java28
-rw-r--r--PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt42
-rw-r--r--PermissionController/src/com/android/permissioncontroller/privacysources/AutoRevokePrivacySource.kt52
-rw-r--r--PermissionController/src/com/android/permissioncontroller/privacysources/LocationAccessPrivacySource.kt2
-rw-r--r--PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt209
-rw-r--r--PermissionController/src/com/android/permissioncontroller/privacysources/SafetyCenterReceiver.kt5
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java29
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java70
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt24
-rw-r--r--PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterViewModel.kt13
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt2
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerPrivacySourceTest.kt15
-rw-r--r--SafetyCenter/Resources/res/raw/safety_center_config.xml10
-rw-r--r--SafetyCenter/Resources/res/values/strings.xml6
-rw-r--r--SafetyCenter/Resources/shared_res/values/strings.xml6
-rw-r--r--SafetyCenter/ResourcesLib/java/com/android/safetycenter/resources/SafetyCenterResourcesContext.java4
-rw-r--r--apex_manifest.json5
-rw-r--r--service/Android.bp38
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterBroadcastDispatcher.java35
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterDataTracker.java218
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterFlags.java146
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java52
-rw-r--r--service/java/com/android/safetycenter/SafetyCenterService.java155
-rw-r--r--service/java/com/android/safetycenter/SafetySources.java2
-rw-r--r--service/java/com/android/safetycenter/UserProfileGroup.java10
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterActivityTest.kt2
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagedDeviceTest.kt226
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt176
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterUnsupportedTest.kt10
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/config/XmlConfigTest.kt2
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterCtsHelper.kt13
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterFlags.kt17
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceCtsData.kt82
-rw-r--r--tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceReceiver.kt7
42 files changed, 1609 insertions, 498 deletions
diff --git a/PermissionController/AndroidManifest.xml b/PermissionController/AndroidManifest.xml
index 46e867666..e598c16d4 100644
--- a/PermissionController/AndroidManifest.xml
+++ b/PermissionController/AndroidManifest.xml
@@ -97,6 +97,9 @@
</intent-filter>
</receiver>
+ <receiver android:name="com.android.permissioncontroller.hibernation.DismissHandler"
+ android:enabled="@bool/is_at_least_t"/>
+
<receiver android:name="com.android.permissioncontroller.permission.service.LocationAccessCheck$NotificationDeleteHandler" />
<receiver android:name="com.android.permissioncontroller.permission.service.LocationAccessCheck$NotificationClickHandler" />
@@ -104,6 +107,9 @@
<receiver android:name="com.android.permissioncontroller.permission.service.LocationAccessCheck$SafetyCenterPrimaryActionHandler"
android:enabled="@bool/is_at_least_t" />
+ <receiver android:name="com.android.permissioncontroller.permission.service.LocationAccessCheck$WarningCardDismissalHandler"
+ android:enabled="@bool/is_at_least_t"/>
+
<receiver android:name="com.android.permissioncontroller.permission.service.LocationAccessCheck$PackageResetHandler"
android:exported="true">
<intent-filter>
@@ -268,6 +274,7 @@
<activity android:name="com.android.permissioncontroller.permission.ui.SafetyCenterQsActivity"
android:excludeFromRecents="true"
+ android:launchMode="singleInstance"
android:exported="true"
android:theme="@style/Theme.SafetyCenterQs"
android:permission="android.permission.REVOKE_RUNTIME_PERMISSIONS">
diff --git a/PermissionController/res/layout-v33/preference_issue_card.xml b/PermissionController/res/layout-v33/preference_issue_card.xml
index 73e9f17e5..ca85bbb3d 100644
--- a/PermissionController/res/layout-v33/preference_issue_card.xml
+++ b/PermissionController/res/layout-v33/preference_issue_card.xml
@@ -38,11 +38,13 @@
android:id="@+id/issue_card_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginEnd="24dp"
android:text="@string/summary_placeholder"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/issue_card_dismiss_btn"
app:layout_constraintHorizontal_bias="0"
+ app:layout_goneMarginEnd="0dp"
style="@style/SafetyCenter.IssueCard.Title" />
<TextView
diff --git a/PermissionController/res/values-v33/dimens.xml b/PermissionController/res/values-v33/dimens.xml
index 95a4d96b9..a697b088b 100644
--- a/PermissionController/res/values-v33/dimens.xml
+++ b/PermissionController/res/values-v33/dimens.xml
@@ -21,4 +21,5 @@
<dimen name="safety_center_indicator_card_icon_margin">28dp</dimen>
<dimen name="safety_center_indicator_expand_button_background">24dp</dimen>
<dimen name="safety_center_top_action_button_margin">24dp</dimen>
+ <dimen name="safety_center_issue_card_dismiss_button_touch_target_size">48dp</dimen>
</resources> \ No newline at end of file
diff --git a/PermissionController/res/values/strings.xml b/PermissionController/res/values/strings.xml
index 2c35bdac7..ece8acb4c 100644
--- a/PermissionController/res/values/strings.xml
+++ b/PermissionController/res/values/strings.xml
@@ -819,6 +819,15 @@
<!-- The notification content for the hibernation reminder notification [CHAR LIMIT=none] -->
<string name="unused_apps_notification_content">Permissions and temporary files have been removed and notifications were stopped. Tap to review.</string>
+ <!-- TODO(b/237446729): The safety center card title for apps being auto-revoked [DO NOT TRANSLATE] [CHAR LIMIT=60] -->
+ <string name="unused_apps_safety_center_card_title">App permissions removed</string>
+
+ <!-- TODO(b/237446729): The safety center card summary for apps being auto-revoked [DO NOT TRANSLATE] [CHAR LIMIT=60] -->
+ <string name="unused_apps_safety_center_card_content">To protect your privacy, permissions from some apps that you haven\u2019t used in a few months have been removed.</string>
+
+ <!-- TODO(b/237446729): The action on the auto-revoked card to see unused apps [DO NOT TRANSLATE] [CHAR LIMIT=60] -->
+ <string name="unused_apps_safety_center_action_title">See unused apps</string>
+
<!-- The notification title for the notification that shows up at the end of a drive where the user made a permission decision [CHAR LIMIT=60] -->
<string name="post_drive_permission_decision_reminder_title">Check recent permissions</string>
diff --git a/PermissionController/src/com/android/permissioncontroller/Constants.java b/PermissionController/src/com/android/permissioncontroller/Constants.java
index 9a91ad36f..dd307e8e0 100644
--- a/PermissionController/src/com/android/permissioncontroller/Constants.java
+++ b/PermissionController/src/com/android/permissioncontroller/Constants.java
@@ -283,6 +283,21 @@ public class Constants {
*/
public static final String OS_PACKAGE_NAME = "android";
+ /**
+ * Source id for safety center source for unused apps.
+ */
+ public static final String UNUSED_APPS_SAFETY_CENTER_SOURCE_ID = "AndroidPermissionAutoRevoke";
+
+ /**
+ * Issue id for safety center issue for unused apps.
+ */
+ public static final String UNUSED_APPS_SAFETY_CENTER_ISSUE_ID = "unused_apps_issue";
+
+ /**
+ * Action id for safety center "See unused apps" action.
+ */
+ public static final String UNUSED_APPS_SAFETY_CENTER_SEE_UNUSED_APPS_ID = "see_unused_apps";
+
// TODO(b/231624295) add to API
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO =
diff --git a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt
index 64481862f..f1306a50f 100644
--- a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt
+++ b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt
@@ -27,6 +27,9 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_IMMUTABLE
+import android.app.PendingIntent.FLAG_ONE_SHOT
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.admin.DeviceAdminReceiver
import android.app.admin.DevicePolicyManager
import android.app.job.JobInfo
@@ -41,9 +44,12 @@ import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.Intent.FLAG_RECEIVER_FOREGROUND
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.os.Build
import android.os.Bundle
import android.os.Process
import android.os.UserHandle
@@ -52,6 +58,11 @@ import android.printservice.PrintService
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION
import android.provider.Settings
+import android.safetycenter.SafetyCenterManager
+import android.safetycenter.SafetyEvent
+import android.safetycenter.SafetySourceData
+import android.safetycenter.SafetySourceIssue
+import android.safetycenter.SafetySourceIssue.Action
import android.service.autofill.AutofillService
import android.service.dreams.DreamService
import android.service.notification.NotificationListenerService
@@ -59,9 +70,11 @@ import android.service.voice.VoiceInteractionService
import android.service.wallpaper.WallpaperService
import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS
import android.telephony.TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS
+import android.text.Html
import android.util.Log
import android.view.inputmethod.InputMethod
import androidx.annotation.MainThread
+import androidx.annotation.RequiresApi
import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager
import com.android.modules.utils.build.SdkLevel
@@ -85,16 +98,17 @@ import com.android.permissioncontroller.permission.data.get
import com.android.permissioncontroller.permission.data.getUnusedPackages
import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo
import com.android.permissioncontroller.permission.service.revokeAppPermissions
+import com.android.permissioncontroller.permission.utils.KotlinUtils
import com.android.permissioncontroller.permission.utils.StringUtils
import com.android.permissioncontroller.permission.utils.Utils
import com.android.permissioncontroller.permission.utils.forEachInParallel
+import java.util.Date
+import java.util.Random
+import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import java.util.Date
-import java.util.Random
-import java.util.concurrent.TimeUnit
private const val LOG_TAG = "HibernationPolicy"
const val DEBUG_OVERRIDE_THRESHOLDS = false
@@ -119,7 +133,9 @@ private fun getCheckFrequencyMs() = DeviceConfig.getLong(
Utils.PROPERTY_HIBERNATION_CHECK_FREQUENCY_MILLIS,
DEFAULT_CHECK_FREQUENCY_MS)
-private val PREF_KEY_FIRST_BOOT_TIME = "first_boot_time"
+private const val PREF_KEY_FIRST_BOOT_TIME = "first_boot_time"
+private const val PREFS_FILE_NAME = "unused_apps_prefs"
+private const val PREF_KEY_UNUSED_APPS_REVIEW = "unused_apps_need_review"
fun isHibernationEnabled(): Boolean {
return SdkLevel.isAtLeastS() &&
@@ -138,6 +154,77 @@ fun hibernationTargetsPreSApps(): Boolean {
}
/**
+ * Remove the unused apps notification.
+ */
+fun cancelUnusedAppsNotification(context: Context) {
+ context.getSystemService(NotificationManager::class.java)!!.cancel(
+ HibernationJobService::class.java.simpleName,
+ Constants.UNUSED_APPS_NOTIFICATION_ID)
+}
+
+/**
+ * Checks if we need to show the safety center card and sends the appropriate source data. If
+ * the user has not reviewed the latest auto-revoked apps, we show the card. Otherwise, we ensure
+ * nothing is shown.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+fun rescanAndPushDataToSafetyCenter(
+ context: Context,
+ sessionId: Long,
+ safetyEvent: SafetyEvent
+) {
+ val safetyCenterManager: SafetyCenterManager =
+ context.getSystemService(SafetyCenterManager::class.java)!!
+ if (getUnusedAppsReviewNeeded(context)) {
+ val seeUnusedAppsAction = Action.Builder(
+ Constants.UNUSED_APPS_SAFETY_CENTER_SEE_UNUSED_APPS_ID,
+ context.getString(R.string.unused_apps_safety_center_action_title),
+ makeUnusedAppsIntent(context, sessionId))
+ .build()
+
+ val issue = SafetySourceIssue.Builder(
+ Constants.UNUSED_APPS_SAFETY_CENTER_ISSUE_ID,
+ context.getString(R.string.unused_apps_safety_center_card_title),
+ context.getString(R.string.unused_apps_safety_center_card_content),
+ SafetySourceData.SEVERITY_LEVEL_INFORMATION,
+ Constants.UNUSED_APPS_SAFETY_CENTER_ISSUE_ID)
+ .addAction(seeUnusedAppsAction)
+ .setOnDismissPendingIntent(makeDismissIntent(context, sessionId))
+ .build()
+
+ val safetySourceData = SafetySourceData.Builder()
+ .addIssue(issue)
+ .build()
+
+ safetyCenterManager.setSafetySourceData(
+ Constants.UNUSED_APPS_SAFETY_CENTER_SOURCE_ID,
+ safetySourceData,
+ safetyEvent)
+ } else {
+ safetyCenterManager.setSafetySourceData(
+ Constants.UNUSED_APPS_SAFETY_CENTER_SOURCE_ID,
+ /* safetySourceData= */ null,
+ safetyEvent)
+ }
+}
+
+/**
+ * Set whether we show the safety center card to the user to review their auto-revoked permissions.
+ */
+fun setUnusedAppsReviewNeeded(context: Context, needsReview: Boolean) {
+ val sharedPreferences = context.sharedPreferences
+ if (sharedPreferences.contains(PREF_KEY_UNUSED_APPS_REVIEW) &&
+ sharedPreferences.getBoolean(PREF_KEY_UNUSED_APPS_REVIEW, false) == needsReview) {
+ return
+ }
+ sharedPreferences.edit().putBoolean(PREF_KEY_UNUSED_APPS_REVIEW, needsReview).apply()
+}
+
+private fun getUnusedAppsReviewNeeded(context: Context): Boolean {
+ return context.sharedPreferences.getBoolean(PREF_KEY_UNUSED_APPS_REVIEW, false)
+}
+
+/**
* Receiver of the onBoot event.
*/
class HibernationOnBootReceiver : BroadcastReceiver() {
@@ -547,6 +634,40 @@ private val Context.firstBootTime: Long get() {
}
/**
+ * Make intent to go to unused apps page.
+ */
+private fun makeUnusedAppsIntent(context: Context, sessionId: Long): PendingIntent {
+ val clickIntent = Intent(Intent.ACTION_MANAGE_UNUSED_APPS).apply {
+ putExtra(Constants.EXTRA_SESSION_ID, sessionId)
+ flags = FLAG_ACTIVITY_NEW_TASK
+ }
+ val pendingIntent = PendingIntent.getActivity(context, 0, clickIntent,
+ FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+ return pendingIntent
+}
+
+/**
+ * Make intent for when safety center card is dismissed.
+ */
+private fun makeDismissIntent(context: Context, sessionId: Long): PendingIntent {
+ val dismissIntent = Intent(context, DismissHandler::class.java).apply {
+ putExtra(Constants.EXTRA_SESSION_ID, sessionId)
+ flags = FLAG_RECEIVER_FOREGROUND
+ }
+ return PendingIntent.getBroadcast(context, /* requestCode= */ 0, dismissIntent,
+ FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
+}
+
+/**
+ * Broadcast receiver class for when safety center card is dismissed.
+ */
+class DismissHandler : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ setUnusedAppsReviewNeeded(context!!, false)
+ }
+}
+
+/**
* A job to check for apps unused in the last [getUnusedThresholdMs]ms every
* [getCheckFrequencyMs]ms and hibernate the app / revoke their runtime permissions.
*/
@@ -589,6 +710,16 @@ class HibernationJobService : JobService() {
val unusedApps: Set<Pair<String, UserHandle>> = hibernatedApps + revokedApps
if (unusedApps.isNotEmpty()) {
showUnusedAppsNotification(unusedApps.size, sessionId)
+ if (SdkLevel.isAtLeastT() &&
+ revokedApps.isNotEmpty() &&
+ getSystemService(SafetyCenterManager::class.java)!!.isSafetyCenterEnabled) {
+ setUnusedAppsReviewNeeded(this@HibernationJobService, true)
+ rescanAndPushDataToSafetyCenter(
+ this@HibernationJobService,
+ sessionId,
+ SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED)
+ .build())
+ }
}
} catch (e: Exception) {
DumpableLog.e(LOG_TAG, "Failed to auto-revoke permissions", e)
@@ -606,14 +737,6 @@ class HibernationJobService : JobService() {
NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(permissionReminderChannel)
- val clickIntent = Intent(Intent.ACTION_MANAGE_UNUSED_APPS).apply {
- putExtra(Constants.EXTRA_SESSION_ID, sessionId)
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- }
- val pendingIntent = PendingIntent.getActivity(this, 0, clickIntent,
- PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or
- PendingIntent.FLAG_IMMUTABLE)
-
var notifTitle: String
var notifContent: String
if (isHibernationEnabled()) {
@@ -630,15 +753,35 @@ class HibernationJobService : JobService() {
.setContentTitle(notifTitle)
.setContentText(notifContent)
.setStyle(Notification.BigTextStyle().bigText(notifContent))
- .setSmallIcon(R.drawable.ic_settings_24dp)
.setColor(getColor(android.R.color.system_notification_accent_color))
.setAutoCancel(true)
- .setContentIntent(pendingIntent)
- Utils.getSettingsLabelForNotifications(applicationContext.packageManager)?.let {
- settingsLabel ->
- val extras = Bundle()
- extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, settingsLabel.toString())
- b.addExtras(extras)
+ .setContentIntent(makeUnusedAppsIntent(this, sessionId))
+ val extras = Bundle()
+ if (SdkLevel.isAtLeastT() &&
+ getSystemService(SafetyCenterManager::class.java)!!.isSafetyCenterEnabled) {
+ if (KotlinUtils.shouldShowSafetyProtectionResources(this)) {
+ // Use Protected by Android branding
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
+ Html.fromHtml(getString(android.R.string.safety_protection_display_text),
+ /* flags= */ 0).toString())
+ b.setSmallIcon(android.R.drawable.ic_safety_protection)
+ .setColor(getColor(R.color.safety_center_info))
+ .addExtras(extras)
+ } else {
+ // Use non-GMS PbA branding
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
+ getString(R.string.safety_center_notification_app_label))
+ b.setSmallIcon(R.drawable.ic_settings_notification)
+ .addExtras(extras)
+ }
+ } else {
+ // Use standard Settings branding
+ Utils.getSettingsLabelForNotifications(applicationContext.packageManager)?.let {
+ settingsLabel ->
+ extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, settingsLabel.toString())
+ b.setSmallIcon(R.drawable.ic_settings_24dp)
+ .addExtras(extras)
+ }
}
notificationManager.notify(HibernationJobService::class.java.simpleName,
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/LocationAccessCheck.java b/PermissionController/src/com/android/permissioncontroller/permission/service/LocationAccessCheck.java
index 0b6fa6452..05d06295c 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/service/LocationAccessCheck.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/service/LocationAccessCheck.java
@@ -55,6 +55,14 @@ import static com.android.permissioncontroller.Constants.PREFERENCES_FILE;
import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION;
import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED;
import static com.android.permissioncontroller.PermissionControllerStatsLog.LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__BG_LOCATION;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN;
+import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__BG_LOCATION;
import static com.android.permissioncontroller.permission.utils.Utils.OS_PKG;
import static com.android.permissioncontroller.permission.utils.Utils.getParcelableExtraSafe;
import static com.android.permissioncontroller.permission.utils.Utils.getParentUserContext;
@@ -465,7 +473,7 @@ public class LocationAccessCheck {
if (isSafetyCenterBgLocationReminderEnabled()) {
SafetyEvent safetyEvent = new SafetyEvent.Builder(
SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED).build();
- sendToSafetyCenter(packages, safetyEvent);
+ sendToSafetyCenter(packages, safetyEvent, null);
}
filterAlreadyNotifiedPackagesLocked(packages);
@@ -699,8 +707,8 @@ public class LocationAccessCheck {
.setStyle(new Notification.BigTextStyle().bigText(notificationContent))
.setSmallIcon(smallIconResId)
.setColor(mContext.getColor(colorResId))
- .setDeleteIntent(createDismissIntent(pkgName, sessionId, uid))
- .setContentIntent(createNotificationClickIntent(pkgName, user, sessionId))
+ .setDeleteIntent(createNotificationDismissIntent(pkgName, sessionId, uid))
+ .setContentIntent(createNotificationClickIntent(pkgName, user, sessionId, uid))
.setAutoCancel(true);
if (!safetyCenterBgLocationReminderEnabled) {
@@ -726,11 +734,20 @@ public class LocationAccessCheck {
if (DEBUG) Log.i(LOG_TAG, "Notified " + pkgName);
- PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
- pkg.applicationInfo.uid, pkgName,
- LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED);
Log.v(LOG_TAG, "Location access check notification shown with sessionId=" + sessionId + ""
+ " uid=" + pkg.applicationInfo.uid + " pkgName=" + pkgName);
+ if (safetyCenterBgLocationReminderEnabled) {
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
+ uid,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN,
+ sessionId);
+ } else {
+ PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
+ pkg.applicationInfo.uid, pkgName,
+ LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_PRESENTED);
+ }
mSharedPrefs.edit().putLong(KEY_LAST_LOCATION_ACCESS_NOTIFICATION_SHOWN,
currentTimeMillis()).apply();
@@ -876,12 +893,21 @@ public class LocationAccessCheck {
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private void sendToSafetyCenter(List<UserPackage> userPackages, SafetyEvent safetyEvent) {
+ private void sendToSafetyCenter(List<UserPackage> userPackages, SafetyEvent safetyEvent,
+ @Nullable UserPackage userPackage) {
try {
Map<UserHandle, List<UserPackage>> userHandleToUserPackagesMap =
splitUserPackageByUserHandle(userPackages);
- userHandleToUserPackagesMap.forEach(
- (userHandle, packages) -> sendUserDataToSafetyCenter(packages, safetyEvent));
+ if (userPackage == null) {
+ userHandleToUserPackagesMap.forEach(
+ (userHandle, packages) -> sendUserDataToSafetyCenter(packages,
+ safetyEvent, null));
+ } else {
+ sendUserDataToSafetyCenter(
+ userHandleToUserPackagesMap.getOrDefault(userPackage.user,
+ new ArrayList<UserPackage>()), safetyEvent, userPackage);
+ }
+
} catch (Exception e) {
Log.e(LOG_TAG, "Could not send to safety center", e);
}
@@ -902,14 +928,14 @@ public class LocationAccessCheck {
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private void sendUserDataToSafetyCenter(List<UserPackage> userPackages,
- SafetyEvent safetyEvent) {
- Context userContext = null;
+ SafetyEvent safetyEvent, @Nullable UserPackage userPackage) {
+ Context userContext = userPackage == null ? null : userPackage.mContext;
SafetySourceData.Builder safetySourceDataBuilder = new SafetySourceData.Builder();
- for (UserPackage userPackage : userPackages) {
+ for (UserPackage userPkg : userPackages) {
if (userContext == null) {
- userContext = userPackage.mContext;
+ userContext = userPkg.mContext;
}
- SafetySourceIssue sourceIssue = createSafetySourceIssue(userPackage);
+ SafetySourceIssue sourceIssue = createSafetySourceIssue(userPkg);
if (sourceIssue != null) {
safetySourceDataBuilder.addIssue(sourceIssue);
}
@@ -938,11 +964,15 @@ public class LocationAccessCheck {
sessionId = new Random().nextLong();
}
+ int uid = pkgInfo.applicationInfo.uid;
+
Intent primaryActionIntent = new Intent(mContext, SafetyCenterPrimaryActionHandler.class);
primaryActionIntent.putExtra(EXTRA_PACKAGE_NAME, userPackage.pkg);
primaryActionIntent.putExtra(EXTRA_USER, userPackage.user);
+ primaryActionIntent.putExtra(EXTRA_UID, uid);
+ primaryActionIntent.putExtra(EXTRA_SESSION_ID, sessionId);
primaryActionIntent.setFlags(FLAG_RECEIVER_FOREGROUND);
- primaryActionIntent.setIdentifier(userPackage.pkg);
+ primaryActionIntent.setIdentifier(userPackage.pkg + userPackage.user);
PendingIntent revokeIntent = PendingIntent.getBroadcast(mContext, 0,
primaryActionIntent,
@@ -976,11 +1006,11 @@ public class LocationAccessCheck {
SafetySourceData.SEVERITY_LEVEL_INFORMATION, id).setSubtitle(
pkgLabel).addAction(revokeAction).addAction(
viewLocationUsageAction).setOnDismissPendingIntent(
- createDismissIntent(pkgName, sessionId, pkgInfo.applicationInfo.uid));
+ createWarningCardDismissalIntent(pkgName, sessionId, uid));
return b.build();
}
- private PendingIntent createDismissIntent(String pkgName, long sessionId, int uid) {
+ private PendingIntent createNotificationDismissIntent(String pkgName, long sessionId, int uid) {
Intent dismissIntent = new Intent(mContext, NotificationDeleteHandler.class);
dismissIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName);
dismissIntent.putExtra(EXTRA_SESSION_ID, sessionId);
@@ -992,22 +1022,35 @@ public class LocationAccessCheck {
}
private PendingIntent createNotificationClickIntent(String pkg, UserHandle user,
- long sessionId) {
+ long sessionId, int uid) {
Intent clickIntent = null;
if (isSafetyCenterBgLocationReminderEnabled()) {
clickIntent = new Intent(ACTION_SAFETY_CENTER);
} else {
clickIntent = new Intent(ACTION_MANAGE_APP_PERMISSION);
clickIntent.putExtra(EXTRA_PERMISSION_GROUP_NAME, LOCATION);
- clickIntent.putExtra(EXTRA_PACKAGE_NAME, pkg);
- clickIntent.putExtra(EXTRA_USER, user);
- clickIntent.putExtra(EXTRA_SESSION_ID, sessionId);
}
+ clickIntent.putExtra(EXTRA_PACKAGE_NAME, pkg);
+ clickIntent.putExtra(EXTRA_USER, user);
+ clickIntent.putExtra(EXTRA_SESSION_ID, sessionId);
+ clickIntent.putExtra(EXTRA_UID, uid);
clickIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK);
return PendingIntent.getActivity(mContext, 0, clickIntent,
FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
}
+ private PendingIntent createWarningCardDismissalIntent(String pkgName, long sessionId,
+ int uid) {
+ Intent dismissIntent = new Intent(mContext, WarningCardDismissalHandler.class);
+ dismissIntent.putExtra(EXTRA_PACKAGE_NAME, pkgName);
+ dismissIntent.putExtra(EXTRA_SESSION_ID, sessionId);
+ dismissIntent.putExtra(EXTRA_UID, uid);
+ dismissIntent.putExtra(EXTRA_USER, getUserHandleForUid(uid));
+ dismissIntent.setFlags(FLAG_RECEIVER_FOREGROUND);
+ return PendingIntent.getBroadcast(mContext, 0, dismissIntent,
+ FLAG_ONE_SHOT | FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE);
+ }
+
/**
* Check if the current user is the profile parent.
*
@@ -1024,16 +1067,18 @@ public class LocationAccessCheck {
* Query for packages having background location access and push to safety center
*
* @param safetyEvent Safety event for which data is being pushed
+ * @param userPackage Optional, if supplied only push safety center data for the user supplied
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
- public void rescanAndPushSafetyCenterData(SafetyEvent safetyEvent) {
+ public void rescanAndPushSafetyCenterData(SafetyEvent safetyEvent,
+ @Nullable UserPackage userPackage) {
if (!isSafetyCenterBgLocationReminderEnabled()) {
return;
}
try {
List<UserPackage> packages = getLocationUsersLocked(mAppOpsManager.getPackagesForOps(
new String[]{OPSTR_FINE_LOCATION}));
- sendToSafetyCenter(packages, safetyEvent);
+ sendToSafetyCenter(packages, safetyEvent, userPackage);
} catch (InterruptedException e) {
Log.e(LOG_TAG, "Couldn't get ops for location");
}
@@ -1170,16 +1215,28 @@ public class LocationAccessCheck {
String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
- int uid = intent.getIntExtra(EXTRA_UID, 0);
+ int uid = intent.getIntExtra(EXTRA_UID, -1);
- PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION, sessionId,
- uid, pkg,
- LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED);
Log.v(LOG_TAG,
"Location access check notification declined with sessionId=" + sessionId + ""
+ " uid=" + uid + " pkgName=" + pkg);
+ LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
- new LocationAccessCheck(context, null).markAsNotified(pkg, user);
+ if (locationAccessCheck.isSafetyCenterBgLocationReminderEnabled()) {
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
+ uid,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED,
+ sessionId
+ );
+ } else {
+ PermissionControllerStatsLog.write(LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION,
+ sessionId,
+ uid, pkg,
+ LOCATION_ACCESS_CHECK_NOTIFICATION_ACTION__RESULT__NOTIFICATION_DECLINED);
+ }
+ locationAccessCheck.markAsNotified(pkg, user);
}
}
@@ -1192,7 +1249,9 @@ public class LocationAccessCheck {
public void onReceive(Context context, Intent intent) {
String packageName = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
- int uid = intent.getIntExtra(EXTRA_UID, 0);
+ int uid = intent.getIntExtra(EXTRA_UID, -1);
+ long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
+ UserPackage userPackage = new UserPackage(context, packageName, user, null);
// Revoke bg location permission and notify safety center
KotlinUtils.INSTANCE.revokeBackgroundRuntimePermissions(context, packageName, LOCATION,
user, () -> {
@@ -1203,8 +1262,15 @@ public class LocationAccessCheck {
createSafetySourceIssueId(packageName))
.setSafetySourceIssueActionId(
createLocationRevokeActionId(packageName))
- .build());
+ .build(), userPackage);
});
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
+ uid,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1,
+ sessionId
+ );
}
}
@@ -1217,6 +1283,33 @@ public class LocationAccessCheck {
}
/**
+ * Handle the case where the warning card is dismissed by the user in Safety center
+ */
+ public static class WarningCardDismissalHandler extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String pkg = getStringExtraSafe(intent, EXTRA_PACKAGE_NAME);
+ UserHandle user = getParcelableExtraSafe(intent, EXTRA_USER);
+ long sessionId = intent.getLongExtra(EXTRA_SESSION_ID, INVALID_SESSION_ID);
+ int uid = intent.getIntExtra(EXTRA_UID, -1);
+ Log.v(LOG_TAG,
+ "Location access check warning card dismissed with sessionId=" + sessionId + ""
+ + " uid=" + uid + " pkgName=" + pkg);
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__BG_LOCATION,
+ uid,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED,
+ sessionId
+ );
+
+ LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
+ locationAccessCheck.markAsNotified(pkg, user);
+ locationAccessCheck.cancelBackgroundAccessWarningNotification(pkg, user);
+ }
+ }
+
+ /**
* If a package gets removed or the data of the package gets cleared, forget that we showed a
* notification for it.
*/
@@ -1233,11 +1326,13 @@ public class LocationAccessCheck {
UserHandle user = getUserHandleForUid(intent.getIntExtra(EXTRA_UID, 0));
if (DEBUG) Log.i(LOG_TAG, "Reset " + data.getSchemeSpecificPart());
LocationAccessCheck locationAccessCheck = new LocationAccessCheck(context, null);
- locationAccessCheck.forgetAboutPackage(data.getSchemeSpecificPart(), user);
+ String packageName = data.getSchemeSpecificPart();
+ locationAccessCheck.forgetAboutPackage(packageName, user);
+ UserPackage userPackage = new UserPackage(context, packageName, user, null);
if (locationAccessCheck.isSafetyCenterBgLocationReminderEnabled()) {
locationAccessCheck.rescanAndPushSafetyCenterData(
new SafetyEvent.Builder(SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED)
- .build());
+ .build(), userPackage);
}
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java
index 4c186cf7e..2aeb92636 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/ManagePermissionsActivity.java
@@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE
import static com.android.permissioncontroller.Constants.ACTION_MANAGE_AUTO_REVOKE;
import static com.android.permissioncontroller.Constants.EXTRA_SESSION_ID;
import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
+import static com.android.permissioncontroller.Constants.UNUSED_APPS_SAFETY_CENTER_SOURCE_ID;
import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION;
import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__OPENED_FOR_AUTO_REVOKE;
import static com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__OPENED_FROM_INTENT;
@@ -38,6 +39,8 @@ import android.os.Bundle;
import android.os.Process;
import android.os.UserHandle;
import android.permission.PermissionManager;
+import android.safetycenter.SafetyCenterManager;
+import android.safetycenter.SafetyEvent;
import android.util.Log;
import android.view.MenuItem;
@@ -47,10 +50,12 @@ import androidx.navigation.NavInflater;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
+import com.android.modules.utils.build.SdkLevel;
import com.android.permissioncontroller.Constants;
import com.android.permissioncontroller.DeviceUtils;
import com.android.permissioncontroller.PermissionControllerStatsLog;
import com.android.permissioncontroller.R;
+import com.android.permissioncontroller.hibernation.HibernationPolicyKt;
import com.android.permissioncontroller.permission.ui.auto.AutoAllAppPermissionsFragment;
import com.android.permissioncontroller.permission.ui.auto.AutoAppPermissionsFragment;
import com.android.permissioncontroller.permission.ui.auto.AutoManageStandardPermissionsFragment;
@@ -376,6 +381,21 @@ public final class ManagePermissionsActivity extends SettingsActivity {
Log.i(LOG_TAG, "sessionId " + sessionId + " starting auto revoke fragment"
+ " from notification");
PermissionControllerStatsLog.write(AUTO_REVOKE_NOTIFICATION_CLICKED, sessionId);
+ if (SdkLevel.isAtLeastT()) {
+ SafetyCenterManager safetyCenterManager =
+ getSystemService(SafetyCenterManager.class);
+ if (safetyCenterManager.isSafetyCenterEnabled()
+ && !safetyCenterManager.getSafetySourceData(
+ UNUSED_APPS_SAFETY_CENTER_SOURCE_ID).getIssues().isEmpty()) {
+ // Clear source data as user has reviewed their unused apps
+ HibernationPolicyKt.setUnusedAppsReviewNeeded(this, false);
+ HibernationPolicyKt.rescanAndPushDataToSafetyCenter(this, sessionId,
+ new SafetyEvent.Builder(
+ SafetyEvent.SAFETY_EVENT_TYPE_SOURCE_STATE_CHANGED)
+ .build());
+ HibernationPolicyKt.cancelUnusedAppsNotification(this);
+ }
+ }
if (DeviceUtils.isAuto(this)) {
androidXFragment = AutoUnusedAppsFragment.newInstance();
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/SafetyCenterQsActivity.java b/PermissionController/src/com/android/permissioncontroller/permission/ui/SafetyCenterQsActivity.java
index 5bdfbb453..cd54f429e 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/SafetyCenterQsActivity.java
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/SafetyCenterQsActivity.java
@@ -18,6 +18,7 @@ package com.android.permissioncontroller.permission.ui;
import static com.android.permissioncontroller.Constants.INVALID_SESSION_ID;
+import android.content.Intent;
import android.os.Bundle;
import android.permission.PermissionGroupUsage;
import android.permission.PermissionManager;
@@ -31,9 +32,7 @@ import com.android.permissioncontroller.permission.ui.handheld.v33.SafetyCenterQ
import java.util.ArrayList;
import java.util.Random;
-/**
- * Activity for the Safety Center Quick Settings Activity
- */
+/** Activity for the Safety Center Quick Settings Activity */
public class SafetyCenterQsActivity extends FragmentActivity {
@Override
@@ -46,13 +45,28 @@ public class SafetyCenterQsActivity extends FragmentActivity {
return;
}
+ configureFragment();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ configureFragment();
+ }
+
+ private void configureFragment() {
long sessionId = getIntent().getLongExtra(Constants.EXTRA_SESSION_ID, INVALID_SESSION_ID);
while (sessionId == INVALID_SESSION_ID) {
sessionId = new Random().nextLong();
}
- ArrayList<PermissionGroupUsage> permissionUsages = getIntent().getParcelableArrayListExtra(
- PermissionManager.EXTRA_PERMISSION_USAGES);
- getSupportFragmentManager().beginTransaction().replace(android.R.id.content,
- SafetyCenterQsFragment.newInstance(sessionId, permissionUsages)).commit();
+ ArrayList<PermissionGroupUsage> permissionUsages =
+ getIntent().getParcelableArrayListExtra(PermissionManager.EXTRA_PERMISSION_USAGES);
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(
+ android.R.id.content,
+ SafetyCenterQsFragment.newInstance(sessionId, permissionUsages))
+ .commit();
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt b/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt
index 1d1328a24..0130bbfe4 100644
--- a/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt
+++ b/PermissionController/src/com/android/permissioncontroller/privacysources/AccessibilitySourceService.kt
@@ -141,6 +141,10 @@ class AccessibilitySourceService(
) {
lock.withLock {
try {
+ var sessionId = Constants.INVALID_SESSION_ID
+ while (sessionId == Constants.INVALID_SESSION_ID) {
+ sessionId = random.nextLong()
+ }
if (DEBUG) {
Log.v(LOG_TAG, "safety center accessibility privacy job started.")
}
@@ -180,12 +184,12 @@ class AccessibilitySourceService(
toBeNotifiedServices[random.nextInt(toBeNotifiedServices.size)]
createPermissionReminderChannel()
interruptJobIfCanceled(cancel)
- sendNotification(serviceToBeNotified)
+ sendNotification(serviceToBeNotified, sessionId)
}
}
interruptJobIfCanceled(cancel)
- sendIssuesToSafetyCenter(a11yServiceList)
+ sendIssuesToSafetyCenter(a11yServiceList, sessionId)
jobService.jobFinished(params, false)
} catch (ex: InterruptedException) {
Log.w(LOG_TAG, "cancel request for safety center accessibility job received.")
@@ -202,14 +206,13 @@ class AccessibilitySourceService(
/**
* sends a notification for a given accessibility package
*/
- private suspend fun sendNotification(serviceToBeNotified: AccessibilityServiceInfo) {
+ private suspend fun sendNotification(
+ serviceToBeNotified: AccessibilityServiceInfo,
+ sessionId: Long
+ ) {
val pkgLabel = serviceToBeNotified.resolveInfo.loadLabel(packageManager)
val componentName = ComponentName.unflattenFromString(serviceToBeNotified.id)!!
val uid = serviceToBeNotified.resolveInfo.serviceInfo.applicationInfo.uid
- var sessionId = Constants.INVALID_SESSION_ID
- while (sessionId == Constants.INVALID_SESSION_ID) {
- sessionId = random.nextLong()
- }
val notificationDeleteIntent =
Intent(parentUserContext, AccessibilityNotificationDeleteHandler::class.java).apply {
@@ -313,16 +316,15 @@ class AccessibilitySourceService(
* @param a11yService enabled 3rd party accessibility service
* @return safety source issue, shown as the warning card in safety center
*/
- private fun createSafetySourceIssue(a11yService: AccessibilityServiceInfo): SafetySourceIssue {
+ private fun createSafetySourceIssue(
+ a11yService: AccessibilityServiceInfo,
+ sessionId: Long
+ ): SafetySourceIssue {
val componentName = ComponentName.unflattenFromString(a11yService.id)!!
val safetySourceIssueId = "accessibility_${componentName.flattenToString()}"
val pkgLabel = a11yService.resolveInfo.loadLabel(packageManager).toString()
val uid = a11yService.resolveInfo.serviceInfo.applicationInfo.uid
- var sessionId = Constants.INVALID_SESSION_ID
- while (sessionId == Constants.INVALID_SESSION_ID) {
- sessionId = random.nextLong()
- }
val removeAccessPendingIntent = getRemoveAccessPendingIntent(
context,
componentName,
@@ -456,11 +458,12 @@ class AccessibilitySourceService(
)
}
- fun sendIssuesToSafetyCenter(
+ private fun sendIssuesToSafetyCenter(
a11yServiceList: List<AccessibilityServiceInfo>,
+ sessionId: Long,
safetyEvent: SafetyEvent = sourceStateChanged
) {
- val pendingIssues = a11yServiceList.map { createSafetySourceIssue(it) }
+ val pendingIssues = a11yServiceList.map { createSafetySourceIssue(it, sessionId) }
val dataBuilder = SafetySourceData.Builder()
pendingIssues.forEach { dataBuilder.addIssue(it) }
val safetySourceData = dataBuilder.build()
@@ -474,6 +477,17 @@ class AccessibilitySourceService(
)
}
+ fun sendIssuesToSafetyCenter(
+ a11yServiceList: List<AccessibilityServiceInfo>,
+ safetyEvent: SafetyEvent = sourceStateChanged
+ ) {
+ var sessionId = Constants.INVALID_SESSION_ID
+ while (sessionId == Constants.INVALID_SESSION_ID) {
+ sessionId = random.nextLong()
+ }
+ sendIssuesToSafetyCenter(a11yServiceList, sessionId, safetyEvent)
+ }
+
fun sendIssuesToSafetyCenter(safetyEvent: SafetyEvent = sourceStateChanged) {
val enabledServices = getEnabledAccessibilityServices()
sendIssuesToSafetyCenter(enabledServices, safetyEvent)
diff --git a/PermissionController/src/com/android/permissioncontroller/privacysources/AutoRevokePrivacySource.kt b/PermissionController/src/com/android/permissioncontroller/privacysources/AutoRevokePrivacySource.kt
new file mode 100644
index 000000000..0660955ff
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/privacysources/AutoRevokePrivacySource.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.permissioncontroller.privacysources
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.android.permissioncontroller.Constants
+import com.android.permissioncontroller.hibernation.cancelUnusedAppsNotification
+import com.android.permissioncontroller.hibernation.rescanAndPushDataToSafetyCenter
+import java.util.Random
+
+/**
+ * Privacy source for auto-revoked permissions.
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class AutoRevokePrivacySource : PrivacySource {
+ override val shouldProcessProfileRequest: Boolean = false
+
+ override fun safetyCenterEnabledChanged(context: Context, enabled: Boolean) {
+ cancelUnusedAppsNotification(context)
+ }
+
+ override fun rescanAndPushSafetyCenterData(
+ context: Context,
+ intent: Intent,
+ refreshEvent: SafetyCenterReceiver.RefreshEvent
+ ) {
+ var sessionId = Constants.INVALID_SESSION_ID
+ while (sessionId == Constants.INVALID_SESSION_ID) {
+ sessionId = Random().nextLong()
+ }
+
+ val safetyRefreshEvent = getSafetyCenterEvent(refreshEvent, intent)
+ rescanAndPushDataToSafetyCenter(context, sessionId, safetyRefreshEvent)
+ }
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/privacysources/LocationAccessPrivacySource.kt b/PermissionController/src/com/android/permissioncontroller/privacysources/LocationAccessPrivacySource.kt
index 860f6a132..df35048e5 100644
--- a/PermissionController/src/com/android/permissioncontroller/privacysources/LocationAccessPrivacySource.kt
+++ b/PermissionController/src/com/android/permissioncontroller/privacysources/LocationAccessPrivacySource.kt
@@ -38,6 +38,6 @@ class LocationAccessPrivacySource : PrivacySource {
refreshEvent: RefreshEvent
) {
val safetyRefreshEvent = getSafetyCenterEvent(refreshEvent, intent)
- LocationAccessCheck(context, null).rescanAndPushSafetyCenterData(safetyRefreshEvent)
+ LocationAccessCheck(context, null).rescanAndPushSafetyCenterData(safetyRefreshEvent, null)
}
} \ No newline at end of file
diff --git a/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt b/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt
index 2fbc34472..79637e2ea 100644
--- a/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt
+++ b/PermissionController/src/com/android/permissioncontroller/privacysources/NotificationListenerCheck.kt
@@ -27,6 +27,7 @@ import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
+import android.app.role.RoleManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
@@ -65,6 +66,15 @@ import com.android.permissioncontroller.Constants.NOTIFICATION_LISTENER_CHECK_AL
import com.android.permissioncontroller.Constants.NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID
import com.android.permissioncontroller.Constants.PERIODIC_NOTIFICATION_LISTENER_CHECK_JOB_ID
import com.android.permissioncontroller.Constants.PREFERENCES_FILE
+import com.android.permissioncontroller.PermissionControllerStatsLog
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN
+import com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER
import com.android.permissioncontroller.R
import com.android.permissioncontroller.permission.utils.Utils
import com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe
@@ -215,12 +225,35 @@ internal class NotificationListenerCheckInternal(
) {
private val parentUserContext = Utils.getParentUserContext(context)
private val random = Random()
+ private val exemptPackages: Set<String> =
+ getExemptedPackages(getSystemServiceSafe(parentUserContext, RoleManager::class.java))
companion object {
@VisibleForTesting const val SC_NLS_ISSUE_TYPE_ID = "notification_listener_privacy_issue"
@VisibleForTesting
const val SC_SHOW_NLS_SETTINGS_ACTION_ID = "show_notification_listener_settings"
+ private const val SYSTEM_PKG = "android"
+
+ private const val SYSTEM_AMBIENT_AUDIO_INTELLIGENCE =
+ "android.app.role.SYSTEM_AMBIENT_AUDIO_INTELLIGENCE"
+ private const val SYSTEM_UI_INTELLIGENCE = "android.app.role.SYSTEM_UI_INTELLIGENCE"
+ private const val SYSTEM_AUDIO_INTELLIGENCE = "android.app.role.SYSTEM_AUDIO_INTELLIGENCE"
+ private const val SYSTEM_NOTIFICATION_INTELLIGENCE =
+ "android.app.role.SYSTEM_NOTIFICATION_INTELLIGENCE"
+ private const val SYSTEM_TEXT_INTELLIGENCE = "android.app.role.SYSTEM_TEXT_INTELLIGENCE"
+ private const val SYSTEM_VISUAL_INTELLIGENCE = "android.app.role.SYSTEM_VISUAL_INTELLIGENCE"
+
+ // This excludes System intelligence roles
+ private val EXEMPTED_ROLES = arrayOf(
+ SYSTEM_AMBIENT_AUDIO_INTELLIGENCE,
+ SYSTEM_UI_INTELLIGENCE,
+ SYSTEM_AUDIO_INTELLIGENCE,
+ SYSTEM_NOTIFICATION_INTELLIGENCE,
+ SYSTEM_TEXT_INTELLIGENCE,
+ SYSTEM_VISUAL_INTELLIGENCE
+ )
+
/** Lock required for all public methods */
private val nlsLock = Mutex()
@@ -265,24 +298,27 @@ internal class NotificationListenerCheckInternal(
// Filter to unnotified components
val unNotifiedComponents = enabledComponents.filter { it !in notifiedComponents }
-
+ var sessionId = Constants.INVALID_SESSION_ID
+ while (sessionId == Constants.INVALID_SESSION_ID) {
+ sessionId = random.nextLong()
+ }
if (DEBUG) {
Log.v(
TAG,
"Found ${enabledComponents.size} enabled notification listeners. " +
"${notifiedComponents.size} already notified. ${unNotifiedComponents.size} " +
- "unnotified")
+ "unnotified, sessionId = $sessionId")
}
throwInterruptedExceptionIfTaskIsCanceled()
- postSystemNotificationIfNeeded(unNotifiedComponents)
- sendIssuesToSafetyCenter(enabledComponents)
+ postSystemNotificationIfNeeded(unNotifiedComponents, sessionId)
+ sendIssuesToSafetyCenter(enabledComponents, sessionId)
}
/**
* Get the [components][ComponentName] which have enabled notification listeners for the
- * parent/context user
+ * parent/context user. Excludes exempt packages.
*
* @throws InterruptedException If [.shouldCancel]
*/
@@ -294,12 +330,32 @@ internal class NotificationListenerCheckInternal(
getSystemServiceSafe(parentUserContext, NotificationManager::class.java)
.enabledNotificationListeners
+ // Filter to components not in exempt packages
+ val enabledNotificationListenersExcludingExemptPackages =
+ enabledNotificationListeners.filter { !exemptPackages.contains(it.packageName) }
+
if (DEBUG) {
- Log.d(TAG, "enabledNotificationListeners = " + "$enabledNotificationListeners")
+ Log.d(
+ TAG,
+ "enabledNotificationListeners=$enabledNotificationListeners\n" +
+ "enabledNotificationListenersExcludingExemptPackages=" +
+ "$enabledNotificationListenersExcludingExemptPackages")
}
throwInterruptedExceptionIfTaskIsCanceled()
- return enabledNotificationListeners
+ return enabledNotificationListenersExcludingExemptPackages
+ }
+
+ /**
+ * Get all the exempted packages.
+ */
+ fun getExemptedPackages(roleManager: RoleManager): Set<String> {
+ val exemptedPackages: MutableSet<String> = HashSet()
+ exemptedPackages.add(SYSTEM_PKG)
+ EXEMPTED_ROLES.forEach { role ->
+ exemptedPackages.addAll(roleManager.getRoleHolders(role))
+ }
+ return exemptedPackages
}
private fun componentHasBeenNotifiedWithinInterval(component: NlsComponent): Boolean {
@@ -451,7 +507,10 @@ internal class NotificationListenerCheckInternal(
}
@Throws(InterruptedException::class)
- private suspend fun postSystemNotificationIfNeeded(components: List<ComponentName>) {
+ private suspend fun postSystemNotificationIfNeeded(
+ components: List<ComponentName>,
+ sessionId: Long
+ ) {
val componentsInternal = components.toMutableList()
// Don't show too many notification within certain timespan
@@ -502,7 +561,7 @@ internal class NotificationListenerCheckInternal(
}
createPermissionReminderChannel()
- createNotificationForNotificationListener(componentToNotifyFor, pkgInfo)
+ createNotificationForNotificationListener(componentToNotifyFor, pkgInfo, sessionId)
markAsNotifiedLocked(componentToNotifyFor)
}
@@ -529,15 +588,17 @@ internal class NotificationListenerCheckInternal(
*/
private fun createNotificationForNotificationListener(
componentName: ComponentName,
- pkg: PackageInfo
+ pkg: PackageInfo,
+ sessionId: Long
) {
val pkgLabel: CharSequence =
Utils.getApplicationLabel(parentUserContext, pkg.applicationInfo)
+ val uid = pkg.applicationInfo.uid
val deletePendingIntent =
- getNotificationDeleteBroadcastPendingIntent(parentUserContext, componentName)
+ getNotificationDeletePendingIntent(parentUserContext, componentName, uid, sessionId)
val clickPendingIntent =
- getSafetyCenterActivityPendingIntent(parentUserContext, componentName)
+ getSafetyCenterActivityPendingIntent(parentUserContext, componentName, uid, sessionId)
val title =
parentUserContext.getString(R.string.notification_listener_reminder_notification_title)
@@ -592,8 +653,15 @@ internal class NotificationListenerCheckInternal(
Log.v(
TAG,
"Notification listener check notification shown with component=" +
- "${componentName.flattenToString()}")
-
+ "${componentName.flattenToString()}, uid=$uid, sessionId=$sessionId")
+
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
+ uid,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_SHOWN,
+ sessionId
+ )
val sharedPrefs: SharedPreferences =
parentUserContext.getSharedPreferences(PREFERENCES_FILE, MODE_PRIVATE)
sharedPrefs
@@ -603,9 +671,11 @@ internal class NotificationListenerCheckInternal(
}
/** @return [PendingIntent] to safety center */
- private fun getNotificationDeleteBroadcastPendingIntent(
+ private fun getNotificationDeletePendingIntent(
context: Context,
- componentName: ComponentName
+ componentName: ComponentName,
+ uid: Int,
+ sessionId: Long
): PendingIntent {
val intent =
Intent(
@@ -613,6 +683,8 @@ internal class NotificationListenerCheckInternal(
NotificationListenerCheckNotificationDeleteHandler::class.java)
.apply {
putExtra(EXTRA_COMPONENT_NAME, componentName)
+ putExtra(Constants.EXTRA_SESSION_ID, sessionId)
+ putExtra(Intent.EXTRA_UID, uid)
flags = FLAG_RECEIVER_FOREGROUND
identifier = componentName.flattenToString()
}
@@ -623,7 +695,9 @@ internal class NotificationListenerCheckInternal(
/** @return [PendingIntent] to safety center */
private fun getSafetyCenterActivityPendingIntent(
context: Context,
- componentName: ComponentName
+ componentName: ComponentName,
+ uid: Int,
+ sessionId: Long
): PendingIntent {
val intent =
Intent(Intent.ACTION_SAFETY_CENTER).apply {
@@ -632,6 +706,8 @@ internal class NotificationListenerCheckInternal(
EXTRA_SAFETY_SOURCE_ISSUE_ID,
getSafetySourceIssueIdFromComponentName(componentName))
putExtra(EXTRA_COMPONENT_NAME, componentName)
+ putExtra(Constants.EXTRA_SESSION_ID, sessionId)
+ putExtra(Intent.EXTRA_UID, uid)
flags = FLAG_ACTIVITY_NEW_TASK
identifier = componentName.flattenToString()
}
@@ -693,14 +769,19 @@ internal class NotificationListenerCheckInternal(
safetyEvent: SafetyEvent = sourceStateChangedSafetyEvent
) {
val enabledComponents = getEnabledNotificationListeners()
- sendIssuesToSafetyCenter(enabledComponents, safetyEvent)
+ var sessionId = Constants.INVALID_SESSION_ID
+ while (sessionId == Constants.INVALID_SESSION_ID) {
+ sessionId = random.nextLong()
+ }
+ sendIssuesToSafetyCenter(enabledComponents, sessionId, safetyEvent)
}
private fun sendIssuesToSafetyCenter(
enabledComponents: List<ComponentName>,
+ sessionId: Long,
safetyEvent: SafetyEvent = sourceStateChangedSafetyEvent
) {
- val pendingIssues = enabledComponents.mapNotNull { createSafetySourceIssue(it) }
+ val pendingIssues = enabledComponents.mapNotNull { createSafetySourceIssue(it, sessionId) }
val dataBuilder = SafetySourceData.Builder()
pendingIssues.forEach { dataBuilder.addIssue(it) }
val safetySourceData = dataBuilder.build()
@@ -715,7 +796,10 @@ internal class NotificationListenerCheckInternal(
* create safety source issue
*/
@VisibleForTesting
- fun createSafetySourceIssue(componentName: ComponentName): SafetySourceIssue? {
+ fun createSafetySourceIssue(
+ componentName: ComponentName,
+ sessionId: Long
+ ): SafetySourceIssue? {
val pkgInfo: PackageInfo
try {
pkgInfo = Utils.getPackageInfoForComponentName(parentUserContext, componentName)
@@ -728,9 +812,10 @@ internal class NotificationListenerCheckInternal(
val pkgLabel: CharSequence =
Utils.getApplicationLabel(parentUserContext, pkgInfo.applicationInfo)
val safetySourceIssueId = getSafetySourceIssueIdFromComponentName(componentName)
+ val uid = pkgInfo.applicationInfo.uid
- val disableNlsPendingIntent =
- getDisableNlsPendingIntent(parentUserContext, safetySourceIssueId, componentName)
+ val disableNlsPendingIntent = getDisableNlsPendingIntent(parentUserContext,
+ safetySourceIssueId, componentName, uid, sessionId)
val disableNlsAction =
SafetySourceIssue.Action.Builder(
@@ -745,7 +830,7 @@ internal class NotificationListenerCheckInternal(
.build()
val notificationListenerSettingsPendingIntent =
- getNotificationListenerSettingsPendingIntent(parentUserContext)
+ getNotificationListenerSettingsPendingIntent(parentUserContext, uid, sessionId)
val showNotificationListenerSettingsAction =
SafetySourceIssue.Action.Builder(
@@ -756,7 +841,7 @@ internal class NotificationListenerCheckInternal(
.build()
val actionCardDismissPendingIntent =
- getActionCardDismissalPendingIntent(parentUserContext, componentName)
+ getActionCardDismissalPendingIntent(parentUserContext, componentName, uid, sessionId)
val title =
parentUserContext.getString(R.string.notification_listener_reminder_notification_title)
@@ -780,37 +865,61 @@ internal class NotificationListenerCheckInternal(
private fun getDisableNlsPendingIntent(
context: Context,
safetySourceIssueId: String,
- componentName: ComponentName
+ componentName: ComponentName,
+ uid: Int,
+ sessionId: Long
): PendingIntent {
val intent =
Intent(context, DisableNotificationListenerComponentHandler::class.java).apply {
putExtra(EXTRA_SAFETY_SOURCE_ISSUE_ID, safetySourceIssueId)
putExtra(EXTRA_COMPONENT_NAME, componentName)
+ putExtra(Constants.EXTRA_SESSION_ID, sessionId)
+ putExtra(Intent.EXTRA_UID, uid)
flags = FLAG_RECEIVER_FOREGROUND
identifier = componentName.flattenToString()
}
- return PendingIntent.getBroadcast(context, 0, intent, FLAG_IMMUTABLE)
+ return PendingIntent.getBroadcast(
+ context,
+ 0,
+ intent,
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
}
/** @return [PendingIntent] to Notification Listener Settings page */
- private fun getNotificationListenerSettingsPendingIntent(context: Context): PendingIntent {
+ private fun getNotificationListenerSettingsPendingIntent(
+ context: Context,
+ uid: Int,
+ sessionId: Long
+ ): PendingIntent {
val intent =
Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS).apply { flags = FLAG_ACTIVITY_NEW_TASK }
- return PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE)
+ intent.putExtra(Constants.EXTRA_SESSION_ID, sessionId)
+ intent.putExtra(Intent.EXTRA_UID, uid)
+ return PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
}
private fun getActionCardDismissalPendingIntent(
context: Context,
- componentName: ComponentName
+ componentName: ComponentName,
+ uid: Int,
+ sessionId: Long
): PendingIntent {
val intent =
Intent(context, NotificationListenerActionCardDismissalReceiver::class.java).apply {
putExtra(EXTRA_COMPONENT_NAME, componentName)
+ putExtra(Constants.EXTRA_SESSION_ID, sessionId)
+ putExtra(Intent.EXTRA_UID, uid)
flags = FLAG_RECEIVER_FOREGROUND
identifier = componentName.flattenToString()
}
- return PendingIntent.getBroadcast(context, 0, intent, FLAG_IMMUTABLE)
+ return PendingIntent.getBroadcast(
+ context,
+ 0,
+ intent,
+ FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
+ )
}
/** If [.shouldCancel] throw an [InterruptedException]. */
@@ -964,13 +1073,24 @@ class NotificationListenerCheckNotificationDeleteHandler : BroadcastReceiver() {
val componentName =
Utils.getParcelableExtraSafe<ComponentName>(intent, EXTRA_COMPONENT_NAME)
+ val sessionId =
+ intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
+ val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
+
GlobalScope.launch(Default) {
NotificationListenerCheckInternal(context, null).markAsNotified(componentName)
}
Log.v(
TAG,
"Notification listener check notification declined with component=" +
- "${componentName.flattenToString()}")
+ "${componentName.flattenToString()} , uid=$uid, sessionId=$sessionId")
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
+ uid,
+ PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__DISMISSED,
+ sessionId
+ )
}
}
@@ -981,10 +1101,14 @@ class DisableNotificationListenerComponentHandler : BroadcastReceiver() {
if (DEBUG) Log.d(TAG, "DisableComponentHandler.onReceive $intent")
val componentName =
Utils.getParcelableExtraSafe<ComponentName>(intent, EXTRA_COMPONENT_NAME)
+ val sessionId =
+ intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
+ val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
GlobalScope.launch(Default) {
if (DEBUG) {
- Log.v(TAG, "DisableComponentHandler: disabling $componentName")
+ Log.v(TAG, "DisableComponentHandler: disabling $componentName," +
+ "uid=$uid, sessionId=$sessionId")
}
val safetyEventBuilder =
@@ -1011,6 +1135,13 @@ class DisableNotificationListenerComponentHandler : BroadcastReceiver() {
removeNotificationsForComponent(componentName)
sendIssuesToSafetyCenter(safetyEvent)
}
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
+ uid,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CLICKED_CTA1,
+ sessionId
+ )
}
}
}
@@ -1022,9 +1153,14 @@ class NotificationListenerActionCardDismissalReceiver : BroadcastReceiver() {
if (DEBUG) Log.d(TAG, "ActionCardDismissalReceiver.onReceive $intent")
val componentName =
Utils.getParcelableExtraSafe<ComponentName>(intent, EXTRA_COMPONENT_NAME)
+ val sessionId =
+ intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID)
+ val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
+
GlobalScope.launch(Default) {
if (DEBUG) {
- Log.v(TAG, "ActionCardDismissalReceiver: $componentName dismissed")
+ Log.v(TAG, "ActionCardDismissalReceiver: $componentName dismissed," +
+ "uid=$uid, sessionId=$sessionId")
}
NotificationListenerCheckInternal(context, null).run {
removeNotificationsForComponent(componentName)
@@ -1032,6 +1168,13 @@ class NotificationListenerActionCardDismissalReceiver : BroadcastReceiver() {
// TODO(b/217566029): update Safety center action cards
}
}
+ PermissionControllerStatsLog.write(
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__PRIVACY_SOURCE__NOTIFICATION_LISTENER,
+ uid,
+ PRIVACY_SIGNAL_ISSUE_CARD_INTERACTION__ACTION__CARD_DISMISSED,
+ sessionId
+ )
}
}
diff --git a/PermissionController/src/com/android/permissioncontroller/privacysources/SafetyCenterReceiver.kt b/PermissionController/src/com/android/permissioncontroller/privacysources/SafetyCenterReceiver.kt
index 4428800f7..e25eb315d 100644
--- a/PermissionController/src/com/android/permissioncontroller/privacysources/SafetyCenterReceiver.kt
+++ b/PermissionController/src/com/android/permissioncontroller/privacysources/SafetyCenterReceiver.kt
@@ -29,12 +29,12 @@ import android.safetycenter.SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHA
import android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS
import androidx.annotation.RequiresApi
import com.android.modules.utils.build.SdkLevel
+import com.android.permissioncontroller.Constants.UNUSED_APPS_SAFETY_CENTER_SOURCE_ID
import com.android.permissioncontroller.PermissionControllerApplication
import com.android.permissioncontroller.permission.service.LocationAccessCheck
import com.android.permissioncontroller.permission.service.v33.SafetyCenterQsTileService
import com.android.permissioncontroller.permission.utils.Utils
import com.android.permissioncontroller.privacysources.WorkPolicyInfo.Companion.WORK_POLICY_INFO_SOURCE_ID
-
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Default
@@ -48,7 +48,8 @@ private fun createMapOfSourceIdsToSources(context: Context): Map<String, Privacy
SC_NLS_SOURCE_ID to NotificationListenerPrivacySource(),
WORK_POLICY_INFO_SOURCE_ID to WorkPolicyInfo.create(context),
SC_ACCESSIBILITY_SOURCE_ID to AccessibilitySourceService(context),
- LocationAccessCheck.BG_LOCATION_SOURCE_ID to LocationAccessPrivacySource()
+ LocationAccessCheck.BG_LOCATION_SOURCE_ID to LocationAccessPrivacySource(),
+ UNUSED_APPS_SAFETY_CENTER_SOURCE_ID to AutoRevokePrivacySource(),
)
}
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java
index 2d0d77dc6..170012c2e 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/IssueCardPreference.java
@@ -25,16 +25,20 @@ import static java.util.Objects.requireNonNull;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
import android.os.Bundle;
import android.safetycenter.SafetyCenterIssue;
import android.text.TextUtils;
import android.util.Log;
+import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
@@ -126,11 +130,36 @@ public class IssueCardPreference extends Preference implements ComparablePrefere
? new ConfirmDismissalOnClickListener()
: new DismissOnClickListener());
dismissButton.setVisibility(View.VISIBLE);
+
+ configureTouchTarget(
+ dismissButton,
+ R.dimen.safety_center_issue_card_dismiss_button_touch_target_size);
} else {
dismissButton.setVisibility(View.GONE);
}
}
+ private void configureTouchTarget(View view, @DimenRes int minTouchTargetSizeResource) {
+ View parent = (View) view.getParent();
+ Resources res = view.getContext().getResources();
+ int minTouchTargetSize = res.getDimensionPixelSize(minTouchTargetSizeResource);
+
+ // Defer getHitRect so that it's called after the parent's children are laid out.
+ parent.post(
+ () -> {
+ Rect hitRect = new Rect();
+ view.getHitRect(hitRect);
+ int currentTouchTargetWidth = hitRect.width();
+ if (currentTouchTargetWidth < minTouchTargetSize) {
+ // inset adjustment is applied to top, bottom, left, right, divide width
+ // difference by two to get adjustment
+ int adjustInsetBy = (minTouchTargetSize - currentTouchTargetWidth) / 2;
+ hitRect.inset(-adjustInsetBy, -adjustInsetBy);
+ parent.setTouchDelegate(new TouchDelegate(hitRect, view));
+ }
+ });
+ }
+
@Override
public boolean isSameItem(@NonNull Preference preference) {
return (preference instanceof IssueCardPreference)
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java
index 8fc60e824..26d0277b8 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java
@@ -22,6 +22,7 @@ import static com.android.permissioncontroller.safetycenter.SafetyCenterConstant
import static java.util.Objects.requireNonNull;
+import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.safetycenter.SafetyCenterData;
@@ -50,6 +51,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.permissioncontroller.R;
import com.android.permissioncontroller.safetycenter.ui.model.LiveSafetyCenterViewModelFactory;
import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel;
+import com.android.safetycenter.resources.SafetyCenterResourcesContext;
import java.util.List;
import java.util.stream.Collectors;
@@ -133,8 +135,8 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
ParsedSafetyCenterIntent parsedSafetyCenterIntent =
ParsedSafetyCenterIntent.toSafetyCenterIntent(getActivity().getIntent());
- mCollapsableIssuesCardHelper
- .setFocusedIssueKey(parsedSafetyCenterIntent.getSafetyCenterIssueKey());
+ mCollapsableIssuesCardHelper.setFocusedIssueKey(
+ parsedSafetyCenterIntent.getSafetyCenterIssueKey());
// Set quick settings state first and allow restored state to override if necessary
mCollapsableIssuesCardHelper.setQuickSettingsState(
@@ -166,18 +168,34 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
mViewModel.getSafetyCenterLiveData().observe(this, this::renderSafetyCenterData);
mViewModel.getErrorLiveData().observe(this, this::displayErrorDetails);
- getLifecycle().addObserver(mViewModel.getAutoRefreshManager());
getPreferenceManager()
.setPreferenceComparisonCallback(new SafetyPreferenceComparisonCallback());
}
@Override
+ public void onStart() {
+ super.onStart();
+ // TODO(b/222323674): We may need to do this in onResume to cover certain edge cases.
+ // i.e. FMD changed from quick settings while SC is open
+ mViewModel.pageOpen();
+ }
+
+ @Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
mCollapsableIssuesCardHelper.saveState(outState);
}
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Activity activity = getActivity();
+ if (activity != null && activity.isChangingConfigurations()) {
+ mViewModel.changingConfigurations();
+ }
+ }
+
SafetyCenterViewModel getSafetyCenterViewModel() {
return mViewModel;
}
@@ -202,26 +220,24 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
updateSafetyEntries(context, data.getEntriesOrGroups());
updateStaticSafetyEntries(context, data.getStaticEntryGroups());
} else {
- setPendingActionState(data);
+ SafetyCenterResourcesContext safetyCenterResourcesContext =
+ new SafetyCenterResourcesContext(context);
+ boolean hasSettingsToReview =
+ safetyCenterResourcesContext
+ .getStringByName("overall_severity_level_ok_review_summary")
+ .equals(data.getStatus().getSummary().toString());
+ setPendingActionState(hasSettingsToReview);
}
}
- /**
- * Determine if there are pending actions and set pending actions state
- */
- private void setPendingActionState(SafetyCenterData data) {
- int overallSeverityLevel = data.getStatus().getSeverityLevel();
- // LINT.IfChange(pendingActionsQs)
- int maxEntrySeverityLevel = getMaxSeverityLevel(data.getEntriesOrGroups());
- if (overallSeverityLevel == SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK
- && maxEntrySeverityLevel > SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK) {
+ /** Determine if there are pending actions and set pending actions state */
+ private void setPendingActionState(boolean hasSettingsToReview) {
+ if (hasSettingsToReview) {
mSafetyStatusPreference.setHasPendingActions(
true, l -> mViewModel.navigateToSafetyCenter(this));
} else {
- mSafetyStatusPreference.setHasPendingActions(
- false, null);
+ mSafetyStatusPreference.setHasPendingActions(false, null);
}
- // LINT.ThenChange(packages/modules/Permission/service/java/com/android/safetycenter/SafetyCenterDataTracker.java:pendingActions)
}
private void displayErrorDetails(@Nullable SafetyCenterErrorDetails errorDetails) {
@@ -267,28 +283,6 @@ public final class SafetyCenterDashboardFragment extends PreferenceFragmentCompa
}
}
- private int getMaxSeverityLevel(List<SafetyCenterEntryOrGroup> entriesOrGroups) {
- int maxEntrySeverityLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN;
- // LINT.IfChange(maxSeverityCalculationQs)
- for (int i = 0, size = entriesOrGroups.size(); i < size; i++) {
- SafetyCenterEntryOrGroup entryOrGroup = entriesOrGroups.get(i);
- SafetyCenterEntry entry = entryOrGroup.getEntry();
- SafetyCenterEntryGroup group = entryOrGroup.getEntryGroup();
-
- if (entry != null) {
- maxEntrySeverityLevel = Math.max(maxEntrySeverityLevel, entry.getSeverityLevel());
- } else if (group != null) {
- List<SafetyCenterEntry> entries = group.getEntries();
- for (SafetyCenterEntry groupEntry : entries) {
- maxEntrySeverityLevel =
- Math.max(maxEntrySeverityLevel, groupEntry.getSeverityLevel());
- }
- }
- }
- return maxEntrySeverityLevel;
- // LINT.ThenChange(packages/modules/Permission/service/java/com/android/safetycenter/SafetyCenterDataTracker.java:maxSeverityCalculation)
- }
-
private void addTopLevelEntry(
Context context,
SafetyCenterEntry entry,
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt
index 7cc410b91..1be632007 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/LiveSafetyCenterViewModel.kt
@@ -27,12 +27,11 @@ import android.safetycenter.SafetyCenterManager
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getMainExecutor
import androidx.fragment.app.Fragment
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import java.util.concurrent.atomic.AtomicBoolean
/* A SafetyCenterViewModel that talks to the real backing service for Safety Center. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -46,6 +45,8 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
private val _safetyCenterLiveData = SafetyCenterLiveData()
private val _errorLiveData = MutableLiveData<SafetyCenterErrorDetails>()
+ private var changingConfigurations = AtomicBoolean(false)
+
private val safetyCenterManager = app.getSystemService(SafetyCenterManager::class.java)!!
override fun dismissIssue(issue: SafetyCenterIssue) {
@@ -69,8 +70,15 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
fragment.startActivity(Intent(ACTION_SAFETY_CENTER))
}
- override fun refresh() {
- safetyCenterManager.refreshSafetySources(SafetyCenterManager.REFRESH_REASON_PAGE_OPEN)
+ override fun pageOpen() {
+ if (!changingConfigurations.getAndSet(false)) {
+ // Refresh unless this is a config change
+ safetyCenterManager.refreshSafetySources(SafetyCenterManager.REFRESH_REASON_PAGE_OPEN)
+ }
+ }
+
+ override fun changingConfigurations() {
+ changingConfigurations.set(true)
}
inner class SafetyCenterLiveData :
@@ -95,14 +103,6 @@ class LiveSafetyCenterViewModel(app: Application) : SafetyCenterViewModel(app) {
_errorLiveData.value = errorDetails
}
}
-
- inner class AutoRefreshManager : DefaultLifecycleObserver {
- // TODO(b/222323674): We may need to do this in onResume to cover certain edge cases.
- // i.e. FMD changed from quick settings while SC is open
- override fun onStart(owner: LifecycleOwner) {
- refresh()
- }
- }
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterViewModel.kt b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterViewModel.kt
index 16b573b2f..c5d4c4e54 100644
--- a/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterViewModel.kt
+++ b/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/model/SafetyCenterViewModel.kt
@@ -24,8 +24,6 @@ import android.safetycenter.SafetyCenterIssue
import androidx.annotation.RequiresApi
import androidx.fragment.app.Fragment
import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -33,7 +31,6 @@ abstract class SafetyCenterViewModel(protected val app: Application) : AndroidVi
abstract val safetyCenterLiveData: LiveData<SafetyCenterData>
abstract val errorLiveData: LiveData<SafetyCenterErrorDetails>
- val autoRefreshManager = AutoRefreshManager()
abstract fun dismissIssue(issue: SafetyCenterIssue)
@@ -45,13 +42,7 @@ abstract class SafetyCenterViewModel(protected val app: Application) : AndroidVi
abstract fun navigateToSafetyCenter(fragment: Fragment)
- protected abstract fun refresh()
+ abstract fun pageOpen()
- inner class AutoRefreshManager : DefaultLifecycleObserver {
- // TODO(b/222323674): We may need to do this in onResume to cover certain edge cases.
- // i.e. FMD changed from quick settings while SC is open
- override fun onStart(owner: LifecycleOwner) {
- refresh()
- }
- }
+ abstract fun changingConfigurations()
}
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt
index 6be2acb82..2addeff66 100644
--- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerCheckInternalTest.kt
@@ -418,7 +418,7 @@ class NotificationListenerCheckInternalTest {
}
val safetySourceIssue = Preconditions.checkNotNull(
- notificationListenerCheck.createSafetySourceIssue(testComponent))
+ notificationListenerCheck.createSafetySourceIssue(testComponent, 0))
val expectedId = "notification_listener_${testComponent.flattenToString()}"
val expectedTitle = context.getString(
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerPrivacySourceTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerPrivacySourceTest.kt
index 2b3656f3c..9c1edb795 100644
--- a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerPrivacySourceTest.kt
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/privacysources/NotificationListenerPrivacySourceTest.kt
@@ -17,6 +17,7 @@
package com.android.permissioncontroller.tests.mocking.privacysources
import android.app.NotificationManager
+import android.app.role.RoleManager
import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
@@ -49,6 +50,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@@ -69,6 +71,8 @@ class NotificationListenerPrivacySourceTest {
@Mock
lateinit var mockNotificationManager: NotificationManager
@Mock
+ lateinit var mockRoleManager: RoleManager
+ @Mock
lateinit var mockUserManager: UserManager
private lateinit var context: Context
@@ -137,6 +141,13 @@ class NotificationListenerPrivacySourceTest {
whenever(mockNotificationManager.enabledNotificationListeners)
.thenReturn(listOf(testComponent1, testComponent2))
+ whenever(Utils.getSystemServiceSafe(
+ any(ContextWrapper::class.java),
+ eq(RoleManager::class.java)))
+ .thenReturn(mockRoleManager)
+ whenever(mockRoleManager.getRoleHolders(anyString()))
+ .thenReturn(emptyList())
+
// Setup Safety Center
whenever(Utils.getSystemServiceSafe(
any(ContextWrapper::class.java),
@@ -285,7 +296,9 @@ class NotificationListenerPrivacySourceTest {
private fun createExpectedSafetyCenterData(): SafetySourceData {
val pendingIssues =
- enabledComponents.mapNotNull { notificationListenerCheck.createSafetySourceIssue(it) }
+ enabledComponents.mapNotNull {
+ notificationListenerCheck.createSafetySourceIssue(it, 0)
+ }
val dataBuilder = SafetySourceData.Builder()
pendingIssues.forEach { dataBuilder.addIssue(it) }
return dataBuilder.build()
diff --git a/SafetyCenter/Resources/res/raw/safety_center_config.xml b/SafetyCenter/Resources/res/raw/safety_center_config.xml
index b05521dbe..ba4197fd0 100644
--- a/SafetyCenter/Resources/res/raw/safety_center_config.xml
+++ b/SafetyCenter/Resources/res/raw/safety_center_config.xml
@@ -16,7 +16,6 @@
<safety-center-config>
<safety-sources-config>
- <!-- TODO(b/214567659): Finalize base XML config -->
<safety-sources-group
id="AndroidLockScreenSources"
title="@com.android.safetycenter.resources:string/lock_screen_sources_title"
@@ -28,14 +27,16 @@
title="@com.android.safetycenter.resources:string/lock_screen_title"
summary="@com.android.safetycenter.resources:string/lock_screen_summary_disabled"
searchTerms="@com.android.safetycenter.resources:string/lock_screen_search_terms"
- initialDisplayState="disabled"/>
+ initialDisplayState="disabled"
+ refreshOnPageOpenAllowed="true"/>
<dynamic-safety-source
id="AndroidBiometrics"
packageName="com.android.settings"
profile="primary_profile_only"
title="@com.android.safetycenter.resources:string/biometrics_title"
searchTerms="@com.android.safetycenter.resources:string/biometrics_search_terms"
- initialDisplayState="hidden"/>
+ initialDisplayState="hidden"
+ refreshOnPageOpenAllowed="true"/>
</safety-sources-group>
<safety-sources-group
id="AndroidPrivacySources"
@@ -91,7 +92,8 @@
id="AndroidWorkPolicyInfo"
packageName="com.android.permissioncontroller"
profile="primary_profile_only"
- initialDisplayState="hidden"/>
+ initialDisplayState="hidden"
+ refreshOnPageOpenAllowed="true"/>
<static-safety-source
id="AndroidAdvancedSecurity"
profile="primary_profile_only"
diff --git a/SafetyCenter/Resources/res/values/strings.xml b/SafetyCenter/Resources/res/values/strings.xml
index 7621f46cf..f47971665 100644
--- a/SafetyCenter/Resources/res/values/strings.xml
+++ b/SafetyCenter/Resources/res/values/strings.xml
@@ -51,4 +51,10 @@
<string name="advanced_privacy_title" description="The title of the entry for advanced privacy settings">More privacy settings</string>
<string name="advanced_privacy_summary" description="The summary of the entry for advanced privacy settings, which describes the page contents">Autofill, activity controls, and more</string>
<string name="advanced_privacy_search_terms" description="Search keywords of the entry for advanced privacy settings"></string>
+
+ <!-- Status -->
+ <!-- Title for the overall Safety Center status when the user security and privacy signals could potentially put their account at risk [CHAR LIMIT=35] -->
+ <string name="overall_severity_level_account_recommendation_title">Account may be at risk</string>
+ <!-- Title for the overall Safety Center status when the user security and privacy signals are putting their account at risk [CHAR LIMIT=35] -->
+ <string name="overall_severity_level_critical_account_warning_title">Account at risk</string>
</resources>
diff --git a/SafetyCenter/Resources/shared_res/values/strings.xml b/SafetyCenter/Resources/shared_res/values/strings.xml
index 1e26a70c6..7ac95b31d 100644
--- a/SafetyCenter/Resources/shared_res/values/strings.xml
+++ b/SafetyCenter/Resources/shared_res/values/strings.xml
@@ -54,5 +54,11 @@
<!-- An error shown to the user when we failed to refresh the overall Safety Center status. This happens when at least one safety signal did not get back to Safety Center within an arbitrary timeout [CHAR LIMIT=50] -->
<string name="refresh_timeout">Couldn\’t refresh status</string>
+
+ <!-- An error shown to the user when we failed to refresh the status of a Safety Center entry [CHAR LIMIT=60] -->
+ <string name="refresh_error">Couldn\’t check status</string>
+
+ <!-- The summary for sources supporting work profile shown to the user in quiet mode. [CHAR LIMIT=NONE] -->
+ <string name="work_profile_paused">Work profile is paused</string>
</resources>
diff --git a/SafetyCenter/ResourcesLib/java/com/android/safetycenter/resources/SafetyCenterResourcesContext.java b/SafetyCenter/ResourcesLib/java/com/android/safetycenter/resources/SafetyCenterResourcesContext.java
index cd44e4a83..81c424937 100644
--- a/SafetyCenter/ResourcesLib/java/com/android/safetycenter/resources/SafetyCenterResourcesContext.java
+++ b/SafetyCenter/ResourcesLib/java/com/android/safetycenter/resources/SafetyCenterResourcesContext.java
@@ -201,14 +201,14 @@ public class SafetyCenterResourcesContext extends ContextWrapper {
* Gets a string resource by name from the Safety Center resources APK, and returns an empty
* string if the resource does not exist.
*/
- @Nullable
+ @NonNull
public String getStringByName(@NonNull String name) {
int id = getStringRes(name);
return emptyIfNamedResourceIsNull(name, getOptionalString(id));
}
/** Same as {@link #getStringByName(String)} but with the given {@code formatArgs}. */
- @Nullable
+ @NonNull
public String getStringByName(@NonNull String name, Object... formatArgs) {
int id = getStringRes(name);
return emptyIfNamedResourceIsNull(name, getOptionalString(id, formatArgs));
diff --git a/apex_manifest.json b/apex_manifest.json
index b3aeab453..b1dbb9798 100644
--- a/apex_manifest.json
+++ b/apex_manifest.json
@@ -1,4 +1,7 @@
{
"name": "com.android.permission",
- "version": 339990000
+
+ // Placeholder module version to be replaced during build.
+ // Do not change!
+ "version": 0
}
diff --git a/service/Android.bp b/service/Android.bp
index 87914f709..5cc31f03f 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -35,30 +35,22 @@ filegroup {
visibility: ["//frameworks/base"],
}
-gensrcs {
+java_library {
name: "service-permission-streaming-proto-java-gen",
- depfile: true,
-
- tools: [
- "aprotoc",
- "protoc-gen-javastream",
- "soong_zip",
- ],
-
- cmd: "mkdir -p $(genDir)/$(in) " +
- "&& $(location aprotoc) " +
- " --plugin=$(location protoc-gen-javastream) " +
- " --dependency_out=$(depfile) " +
- " --javastream_out=$(genDir)/$(in) " +
- " -Iexternal/protobuf/src " +
- " -I . " +
- " $(in) " +
- "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
-
- srcs: [
- ":service-permission-streaming-proto-sources",
+ proto: {
+ type: "stream",
+ include_dirs: [
+ "external/protobuf/src",
+ ],
+ },
+ srcs: [":service-permission-streaming-proto-sources"],
+ installable: false,
+ min_sdk_version: "30",
+ sdk_version: "system_server_current",
+ apex_available: [
+ "com.android.permission",
+ "test_com.android.permission",
],
- output_extension: "srcjar",
}
java_library {
@@ -91,7 +83,6 @@ java_sdk_library {
],
srcs: [
":service-permission-java-sources",
- ":service-permission-streaming-proto-java-gen",
],
libs: [
"androidx.annotation_annotation",
@@ -117,6 +108,7 @@ java_sdk_library {
"safety-center-persistence",
"safety-center-resources-lib",
"service-permission-shared",
+ "service-permission-streaming-proto-java-gen",
],
exclude_kotlinc_generated_files: true,
jarjar_rules: ":permission-jarjar-rules",
diff --git a/service/java/com/android/safetycenter/SafetyCenterBroadcastDispatcher.java b/service/java/com/android/safetycenter/SafetyCenterBroadcastDispatcher.java
index 850d896b3..1690f4aee 100644
--- a/service/java/com/android/safetycenter/SafetyCenterBroadcastDispatcher.java
+++ b/service/java/com/android/safetycenter/SafetyCenterBroadcastDispatcher.java
@@ -43,7 +43,6 @@ import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.UserHandle;
-import android.provider.DeviceConfig;
import android.safetycenter.SafetyCenterManager;
import android.safetycenter.SafetyCenterManager.RefreshReason;
import android.safetycenter.SafetyCenterManager.RefreshRequestType;
@@ -63,22 +62,6 @@ import java.util.List;
@RequiresApi(TIRAMISU)
final class SafetyCenterBroadcastDispatcher {
- private static final String TAG = "SafetyCenterBroadcastDi";
-
- /**
- * Device Config flag that determines 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.
- */
- private static final String PROPERTY_FGS_ALLOWLIST_DURATION_MILLIS =
- "safety_center_refresh_fgs_allowlist_duration_millis";
-
- /**
- * Default time for which an app, upon receiving a particular broadcast, will be placed on a
- * temporary power allowlist allowing it to start a foreground service from the background.
- */
- private static final Duration FGS_ALLOWLIST_DEFAULT_DURATION = Duration.ofSeconds(20);
-
@NonNull private final Context mContext;
/** Creates a {@link SafetyCenterBroadcastDispatcher} using the given {@link Context}. */
@@ -261,12 +244,12 @@ final class SafetyCenterBroadcastDispatcher {
@NonNull
private static BroadcastOptions createBroadcastOptions() {
BroadcastOptions broadcastOptions = BroadcastOptions.makeBasic();
- // The following operation requires the START_FOREGROUND_SERVICES_FROM_BACKGROUND
- // and READ_DEVICE_CONFIG permissions.
+ Duration allowListDuration = SafetyCenterFlags.getFgsAllowlistDuration();
+ // The following operation requires the START_FOREGROUND_SERVICES_FROM_BACKGROUND.
final long callingId = Binder.clearCallingIdentity();
try {
broadcastOptions.setTemporaryAppAllowlist(
- getFgsAllowlistDuration().toMillis(),
+ allowListDuration.toMillis(),
TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
REASON_REFRESH_SAFETY_SOURCES,
"Safety Center is requesting data from safety sources");
@@ -290,16 +273,4 @@ final class SafetyCenterBroadcastDispatcher {
}
throw new IllegalArgumentException("Unexpected refresh reason: " + refreshReason);
}
-
- /**
- * Returns the time for which an app, upon receiving a particular broadcast, should be placed on
- * a temporary power allowlist allowing it to start a foreground service from the background.
- */
- private static Duration getFgsAllowlistDuration() {
- return Duration.ofMillis(
- DeviceConfig.getLong(
- DeviceConfig.NAMESPACE_PRIVACY,
- PROPERTY_FGS_ALLOWLIST_DURATION_MILLIS,
- FGS_ALLOWLIST_DEFAULT_DURATION.toMillis()));
- }
}
diff --git a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
index 773a43814..fe0b6768c 100644
--- a/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
+++ b/service/java/com/android/safetycenter/SafetyCenterDataTracker.java
@@ -72,6 +72,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.Set;
import javax.annotation.concurrent.NotThreadSafe;
@@ -95,6 +96,7 @@ final class SafetyCenterDataTracker {
private final ArrayMap<SafetySourceKey, SafetySourceData> mSafetySourceDataForKey =
new ArrayMap<>();
+ private final ArraySet<SafetySourceKey> mSafetySourceErrors = new ArraySet<>();
private final ArrayMap<SafetyCenterIssueKey, IssueData> mSafetyCenterIssueCache =
new ArrayMap<>();
@@ -212,13 +214,15 @@ final class SafetyCenterDataTracker {
if (!validateRequest(safetySourceData, safetySourceId, packageName, userId)) {
return false;
}
- boolean safetyCenterDataHasChanged =
+ boolean safetyEventChangedSafetyCenterData =
processSafetyEvent(safetySourceId, safetyEvent, userId);
SafetySourceKey key = SafetySourceKey.of(safetySourceId, userId);
+ boolean removingSafetySourceErrorChangedSafetyCenterData = mSafetySourceErrors.remove(key);
SafetySourceData existingSafetySourceData = mSafetySourceDataForKey.get(key);
if (Objects.equals(safetySourceData, existingSafetySourceData)) {
- return safetyCenterDataHasChanged;
+ return safetyEventChangedSafetyCenterData
+ || removingSafetySourceErrorChangedSafetyCenterData;
}
ArraySet<String> issueIds = new ArraySet<>();
@@ -270,14 +274,38 @@ final class SafetyCenterDataTracker {
if (!validateRequest(null, safetySourceId, packageName, userId)) {
return false;
}
- Log.w(
- TAG,
- "Error reported from source: "
- + safetySourceId
- + ", for event: "
- + safetySourceErrorDetails.getSafetyEvent());
- return processSafetyEvent(
- safetySourceId, safetySourceErrorDetails.getSafetyEvent(), userId);
+ SafetyEvent safetyEvent = safetySourceErrorDetails.getSafetyEvent();
+ Log.w(TAG, "Error reported from source: " + safetySourceId + ", for event: " + safetyEvent);
+
+ boolean safetyEventChangedSafetyCenterData =
+ processSafetyEvent(safetySourceId, safetyEvent, userId);
+ 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. */
+ boolean setSafetySourceError(@NonNull SafetySourceKey safetySourceKey) {
+ boolean removingSafetySourceDataChangedSafetyCenterData =
+ mSafetySourceDataForKey.remove(safetySourceKey) != null;
+ boolean addingSafetySourceErrorChangedSafetyCenterData =
+ mSafetySourceErrors.add(safetySourceKey);
+ return removingSafetySourceDataChangedSafetyCenterData
+ || addingSafetySourceErrorChangedSafetyCenterData;
+ }
+
+ /**
+ * Clears all safety source errors received so far, this is useful e.g. when starting a new
+ * broadcast.
+ */
+ void clearSafetySourceErrors() {
+ mSafetySourceErrors.clear();
}
/**
@@ -403,6 +431,7 @@ final class SafetyCenterDataTracker {
getSafetyCenterStatusTitle(
SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN,
SafetyCenterStatus.REFRESH_STATUS_NONE,
+ new ArraySet<>(),
false),
getSafetyCenterStatusSummary(
SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN,
@@ -417,7 +446,7 @@ final class SafetyCenterDataTracker {
}
/**
- * Clears all the {@link SafetySourceData}, metadata associated with {@link
+ * Clears all the {@link SafetySourceData} and errors, metadata associated with {@link
* SafetyCenterIssueKey}s, in flight {@link SafetyCenterIssueActionId} and any refresh in
* progress so far, for all users.
*
@@ -426,6 +455,7 @@ final class SafetyCenterDataTracker {
*/
void clear() {
mSafetySourceDataForKey.clear();
+ mSafetySourceErrors.clear();
mSafetyCenterIssueCache.clear();
mSafetyCenterIssueCacheDirty = true;
mSafetyCenterIssueActionsInFlight.clear();
@@ -675,8 +705,11 @@ final class SafetyCenterDataTracker {
int safetyCenterOverallSeverityLevel = SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK;
int safetyCenterEntriesSeverityLevel = SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK;
List<SafetyCenterIssue> safetyCenterIssues = new ArrayList<>();
+ Set<Integer> allCurrentIssueCategories = new ArraySet<>();
List<SafetyCenterEntryOrGroup> safetyCenterEntryOrGroups = new ArrayList<>();
List<SafetyCenterStaticEntryGroup> safetyCenterStaticEntryGroups = new ArrayList<>();
+ SafetyCenterOverallStatusErrorState safetyCenterOverallStatusErrorState =
+ new SafetyCenterOverallStatusErrorState();
for (int i = 0; i < safetySourcesGroups.size(); i++) {
SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i);
@@ -685,7 +718,10 @@ final class SafetyCenterDataTracker {
Math.max(
safetyCenterOverallSeverityLevel,
addSafetyCenterIssues(
- safetyCenterIssues, safetySourcesGroup, userProfileGroup));
+ safetyCenterIssues,
+ allCurrentIssueCategories,
+ safetySourcesGroup,
+ userProfileGroup));
int safetySourcesGroupType = safetySourcesGroup.getType();
switch (safetySourcesGroupType) {
case SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_COLLAPSIBLE:
@@ -695,6 +731,7 @@ final class SafetyCenterDataTracker {
addSafetyCenterEntryGroup(
safetyCenterEntryOrGroups,
safetySourcesGroup,
+ safetyCenterOverallStatusErrorState,
packageName,
userProfileGroup));
break;
@@ -702,6 +739,7 @@ final class SafetyCenterDataTracker {
addSafetyCenterStaticEntryGroup(
safetyCenterStaticEntryGroups,
safetySourcesGroup,
+ safetyCenterOverallStatusErrorState,
packageName,
userProfileGroup);
break;
@@ -713,10 +751,9 @@ final class SafetyCenterDataTracker {
}
}
- // LINT.IfChange(pendingActions)
boolean hasSettingsToReview =
- safetyCenterEntriesSeverityLevel > safetyCenterOverallSeverityLevel;
- // LINT.ThenChange(packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java:pendingActionsQs)
+ safetyCenterEntriesSeverityLevel > safetyCenterOverallSeverityLevel
+ || safetyCenterOverallStatusErrorState.mHasAtLeastOneUserVisibleError;
safetyCenterIssues.sort(SAFETY_CENTER_ISSUES_BY_SEVERITY_DESCENDING);
int refreshStatus = mSafetyCenterRefreshTracker.getRefreshStatus();
return new SafetyCenterData(
@@ -724,6 +761,7 @@ final class SafetyCenterDataTracker {
getSafetyCenterStatusTitle(
safetyCenterOverallSeverityLevel,
refreshStatus,
+ allCurrentIssueCategories,
hasSettingsToReview),
getSafetyCenterStatusSummary(
safetyCenterOverallSeverityLevel,
@@ -741,6 +779,7 @@ final class SafetyCenterDataTracker {
@SafetyCenterStatus.OverallSeverityLevel
private int addSafetyCenterIssues(
@NonNull List<SafetyCenterIssue> safetyCenterIssues,
+ @NonNull Set<Integer> allCurrentIssueCategories,
@NonNull SafetySourcesGroup safetySourcesGroup,
@NonNull UserProfileGroup userProfileGroup) {
int safetyCenterIssuesOverallSeverityLevel = SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK;
@@ -757,6 +796,7 @@ final class SafetyCenterDataTracker {
safetyCenterIssuesOverallSeverityLevel,
addSafetyCenterIssues(
safetyCenterIssues,
+ allCurrentIssueCategories,
safetySource,
userProfileGroup.getProfileParentUserId()));
@@ -764,15 +804,19 @@ final class SafetyCenterDataTracker {
continue;
}
- int[] managedProfilesUserIds = userProfileGroup.getManagedProfilesUserIds();
- for (int j = 0; j < managedProfilesUserIds.length; j++) {
- int managedProfileUserId = managedProfilesUserIds[j];
+ int[] managedRunningProfilesUserIds =
+ userProfileGroup.getManagedRunningProfilesUserIds();
+ for (int j = 0; j < managedRunningProfilesUserIds.length; j++) {
+ int managedRunningProfileUserId = managedRunningProfilesUserIds[j];
safetyCenterIssuesOverallSeverityLevel =
Math.max(
safetyCenterIssuesOverallSeverityLevel,
addSafetyCenterIssues(
- safetyCenterIssues, safetySource, managedProfileUserId));
+ safetyCenterIssues,
+ allCurrentIssueCategories,
+ safetySource,
+ managedRunningProfileUserId));
}
}
@@ -782,6 +826,7 @@ final class SafetyCenterDataTracker {
@SafetyCenterStatus.OverallSeverityLevel
private int addSafetyCenterIssues(
@NonNull List<SafetyCenterIssue> safetyCenterIssues,
+ @NonNull Set<Integer> allCurrentIssueCategories,
@NonNull SafetySource safetySource,
@UserIdInt int userId) {
SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId);
@@ -796,7 +841,6 @@ final class SafetyCenterDataTracker {
List<SafetySourceIssue> safetySourceIssues = safetySourceData.getIssues();
for (int i = 0; i < safetySourceIssues.size(); i++) {
SafetySourceIssue safetySourceIssue = safetySourceIssues.get(i);
- // LINT.IfChange(maxSeverityCalculation)
SafetyCenterIssue safetyCenterIssue =
toSafetyCenterIssue(safetySourceIssue, safetySource, userId);
if (safetyCenterIssue == null) {
@@ -808,10 +852,11 @@ final class SafetyCenterDataTracker {
toSafetyCenterStatusOverallSeverityLevel(
safetySourceIssue.getSeverityLevel()));
safetyCenterIssues.add(safetyCenterIssue);
+
+ allCurrentIssueCategories.add(safetySourceIssue.getIssueCategory());
}
return safetyCenterIssuesOverallSeverityLevel;
- // LINT.ThenChange(packages/modules/Permission/PermissionController/src/com/android/permissioncontroller/safetycenter/ui/SafetyCenterDashboardFragment.java:maxSeverityCalculationQs)
}
@Nullable
@@ -883,6 +928,7 @@ final class SafetyCenterDataTracker {
private int addSafetyCenterEntryGroup(
@NonNull List<SafetyCenterEntryOrGroup> safetyCenterEntryOrGroups,
@NonNull SafetySourcesGroup safetySourcesGroup,
+ @NonNull SafetyCenterOverallStatusErrorState safetyCenterOverallStatusErrorState,
@NonNull String defaultPackageName,
@NonNull UserProfileGroup userProfileGroup) {
int groupSafetyCenterEntryLevel = SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN;
@@ -898,9 +944,11 @@ final class SafetyCenterDataTracker {
addSafetyCenterEntry(
entries,
safetySource,
+ safetyCenterOverallStatusErrorState,
defaultPackageName,
+ userProfileGroup.getProfileParentUserId(),
false,
- userProfileGroup.getProfileParentUserId()));
+ false));
if (!SafetySources.supportsManagedProfiles(safetySource)) {
continue;
@@ -909,6 +957,8 @@ final class SafetyCenterDataTracker {
int[] managedProfilesUserIds = userProfileGroup.getManagedProfilesUserIds();
for (int j = 0; j < managedProfilesUserIds.length; j++) {
int managedProfileUserId = managedProfilesUserIds[j];
+ boolean isManagedUserRunning =
+ userProfileGroup.isManagedUserRunning(managedProfileUserId);
groupSafetyCenterEntryLevel =
Math.max(
@@ -916,9 +966,11 @@ final class SafetyCenterDataTracker {
addSafetyCenterEntry(
entries,
safetySource,
+ safetyCenterOverallStatusErrorState,
defaultPackageName,
+ managedProfileUserId,
true,
- managedProfileUserId));
+ isManagedUserRunning));
}
}
@@ -978,15 +1030,24 @@ final class SafetyCenterDataTracker {
private int addSafetyCenterEntry(
@NonNull List<SafetyCenterEntry> entries,
@NonNull SafetySource safetySource,
+ @NonNull SafetyCenterOverallStatusErrorState safetyCenterOverallStatusErrorState,
@NonNull String defaultPackageName,
+ @UserIdInt int userId,
boolean isUserManaged,
- @UserIdInt int userId) {
+ boolean isManagedUserRunning) {
SafetyCenterEntry safetyCenterEntry =
- toSafetyCenterEntry(safetySource, defaultPackageName, isUserManaged, userId);
+ toSafetyCenterEntry(
+ safetySource,
+ defaultPackageName,
+ userId,
+ isUserManaged,
+ isManagedUserRunning);
if (safetyCenterEntry == null) {
return SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN;
}
+ safetyCenterOverallStatusErrorState.mHasAtLeastOneUserVisibleError |=
+ mSafetySourceErrors.contains(SafetySourceKey.of(safetySource.getId(), userId));
entries.add(safetyCenterEntry);
return safetyCenterEntry.getSeverityLevel();
@@ -996,8 +1057,9 @@ final class SafetyCenterDataTracker {
private SafetyCenterEntry toSafetyCenterEntry(
@NonNull SafetySource safetySource,
@NonNull String defaultPackageName,
+ @UserIdInt int userId,
boolean isUserManaged,
- @UserIdInt int userId) {
+ boolean isManagedUserRunning) {
switch (safetySource.getType()) {
case SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY:
return null;
@@ -1005,7 +1067,8 @@ final class SafetyCenterDataTracker {
SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId);
SafetySourceStatus safetySourceStatus =
getSafetySourceStatus(mSafetySourceDataForKey.get(key));
- if (safetySourceStatus != null) {
+ boolean defaultEntryDueToQuietMode = isUserManaged && !isManagedUserRunning;
+ if (safetySourceStatus != null && !defaultEntryDueToQuietMode) {
PendingIntent pendingIntent = safetySourceStatus.getPendingIntent();
boolean enabled = safetySourceStatus.isEnabled();
if (pendingIntent == null) {
@@ -1048,16 +1111,18 @@ final class SafetyCenterDataTracker {
safetySource.getPackageName(),
SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN,
SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION,
+ userId,
isUserManaged,
- userId);
+ isManagedUserRunning);
case SafetySource.SAFETY_SOURCE_TYPE_STATIC:
return toDefaultSafetyCenterEntry(
safetySource,
defaultPackageName,
SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED,
SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON,
+ userId,
isUserManaged,
- userId);
+ isManagedUserRunning);
}
Log.w(
TAG,
@@ -1071,8 +1136,9 @@ final class SafetyCenterDataTracker {
@NonNull String packageName,
@SafetyCenterEntry.EntrySeverityLevel int entrySeverityLevel,
@SafetyCenterEntry.SeverityUnspecifiedIconType int severityUnspecifiedIconType,
+ @UserIdInt int userId,
boolean isUserManaged,
- @UserIdInt int userId) {
+ boolean isManagedUserRunning) {
if (SafetySources.isDefaultEntryHidden(safetySource)) {
return null;
}
@@ -1091,12 +1157,19 @@ final class SafetyCenterDataTracker {
isUserManaged
? safetySource.getTitleForWorkResId()
: safetySource.getTitleResId());
+ CharSequence summary =
+ mSafetySourceErrors.contains(SafetySourceKey.of(safetySource.getId(), userId))
+ ? mSafetyCenterResourcesContext.getStringByName("refresh_error")
+ : mSafetyCenterResourcesContext.getOptionalString(
+ safetySource.getSummaryResId());
+ if (isUserManaged && !isManagedUserRunning) {
+ enabled = false;
+ summary = mSafetyCenterResourcesContext.getStringByName("work_profile_paused");
+ }
return new SafetyCenterEntry.Builder(
SafetyCenterIds.encodeToString(safetyCenterEntryId), title)
.setSeverityLevel(entrySeverityLevel)
- .setSummary(
- mSafetyCenterResourcesContext.getOptionalString(
- safetySource.getSummaryResId()))
+ .setSummary(summary)
.setEnabled(enabled)
.setPendingIntent(pendingIntent)
.setSeverityUnspecifiedIconType(severityUnspecifiedIconType)
@@ -1106,6 +1179,7 @@ final class SafetyCenterDataTracker {
private void addSafetyCenterStaticEntryGroup(
@NonNull List<SafetyCenterStaticEntryGroup> safetyCenterStaticEntryGroups,
@NonNull SafetySourcesGroup safetySourcesGroup,
+ @NonNull SafetyCenterOverallStatusErrorState safetyCenterOverallStatusErrorState,
@NonNull String defaultPackageName,
@NonNull UserProfileGroup userProfileGroup) {
List<SafetySource> safetySources = safetySourcesGroup.getSafetySources();
@@ -1116,9 +1190,11 @@ final class SafetyCenterDataTracker {
addSafetyCenterStaticEntry(
staticEntries,
safetySource,
+ safetyCenterOverallStatusErrorState,
defaultPackageName,
+ userProfileGroup.getProfileParentUserId(),
false,
- userProfileGroup.getProfileParentUserId());
+ false);
if (!SafetySources.supportsManagedProfiles(safetySource)) {
continue;
@@ -1127,13 +1203,17 @@ final class SafetyCenterDataTracker {
int[] managedProfilesUserIds = userProfileGroup.getManagedProfilesUserIds();
for (int j = 0; j < managedProfilesUserIds.length; j++) {
int managedProfileUserId = managedProfilesUserIds[j];
+ boolean isManagedUserRunning =
+ userProfileGroup.isManagedUserRunning(managedProfileUserId);
addSafetyCenterStaticEntry(
staticEntries,
safetySource,
+ safetyCenterOverallStatusErrorState,
defaultPackageName,
+ managedProfileUserId,
true,
- managedProfileUserId);
+ isManagedUserRunning);
}
}
@@ -1146,14 +1226,23 @@ final class SafetyCenterDataTracker {
private void addSafetyCenterStaticEntry(
@NonNull List<SafetyCenterStaticEntry> staticEntries,
@NonNull SafetySource safetySource,
+ @NonNull SafetyCenterOverallStatusErrorState safetyCenterOverallStatusErrorState,
@NonNull String defaultPackageName,
+ @UserIdInt int userId,
boolean isUserManaged,
- @UserIdInt int userId) {
+ boolean isManagedUserRunning) {
SafetyCenterStaticEntry staticEntry =
- toSafetyCenterStaticEntry(safetySource, defaultPackageName, isUserManaged, userId);
+ toSafetyCenterStaticEntry(
+ safetySource,
+ defaultPackageName,
+ userId,
+ isUserManaged,
+ isManagedUserRunning);
if (staticEntry == null) {
return;
}
+ safetyCenterOverallStatusErrorState.mHasAtLeastOneUserVisibleError |=
+ mSafetySourceErrors.contains(SafetySourceKey.of(safetySource.getId(), userId));
staticEntries.add(staticEntry);
}
@@ -1161,8 +1250,9 @@ final class SafetyCenterDataTracker {
private SafetyCenterStaticEntry toSafetyCenterStaticEntry(
@NonNull SafetySource safetySource,
@NonNull String defaultPackageName,
+ @UserIdInt int userId,
boolean isUserManaged,
- @UserIdInt int userId) {
+ boolean isManagedUserRunning) {
switch (safetySource.getType()) {
case SafetySource.SAFETY_SOURCE_TYPE_ISSUE_ONLY:
return null;
@@ -1170,7 +1260,8 @@ final class SafetyCenterDataTracker {
SafetySourceKey key = SafetySourceKey.of(safetySource.getId(), userId);
SafetySourceStatus safetySourceStatus =
getSafetySourceStatus(mSafetySourceDataForKey.get(key));
- if (safetySourceStatus != null) {
+ boolean defaultEntryDueToQuietMode = isUserManaged && !isManagedUserRunning;
+ if (safetySourceStatus != null && !defaultEntryDueToQuietMode) {
PendingIntent pendingIntent = safetySourceStatus.getPendingIntent();
if (pendingIntent == null) {
// TODO(b/222838784): Decide strategy for static entries when the intent is
@@ -1183,10 +1274,18 @@ final class SafetyCenterDataTracker {
.build();
}
return toDefaultSafetyCenterStaticEntry(
- safetySource, safetySource.getPackageName(), isUserManaged, userId);
+ safetySource,
+ safetySource.getPackageName(),
+ userId,
+ isUserManaged,
+ isManagedUserRunning);
case SafetySource.SAFETY_SOURCE_TYPE_STATIC:
return toDefaultSafetyCenterStaticEntry(
- safetySource, defaultPackageName, isUserManaged, userId);
+ safetySource,
+ defaultPackageName,
+ userId,
+ isUserManaged,
+ isManagedUserRunning);
}
Log.w(TAG, "Unknown safety source type found in rigid group: " + safetySource.getType());
return null;
@@ -1196,8 +1295,9 @@ final class SafetyCenterDataTracker {
private SafetyCenterStaticEntry toDefaultSafetyCenterStaticEntry(
@NonNull SafetySource safetySource,
@NonNull String packageName,
+ @UserIdInt int userId,
boolean isUserManaged,
- @UserIdInt int userId) {
+ boolean isManagedUserRunning) {
if (SafetySources.isDefaultEntryHidden(safetySource)) {
return null;
}
@@ -1215,11 +1315,16 @@ final class SafetyCenterDataTracker {
isUserManaged
? safetySource.getTitleForWorkResId()
: safetySource.getTitleResId());
-
+ CharSequence summary =
+ mSafetySourceErrors.contains(SafetySourceKey.of(safetySource.getId(), userId))
+ ? mSafetyCenterResourcesContext.getStringByName("refresh_error")
+ : mSafetyCenterResourcesContext.getOptionalString(
+ safetySource.getSummaryResId());
+ if (isUserManaged && !isManagedUserRunning) {
+ summary = mSafetyCenterResourcesContext.getStringByName("work_profile_paused");
+ }
return new SafetyCenterStaticEntry.Builder(title)
- .setSummary(
- mSafetyCenterResourcesContext.getOptionalString(
- safetySource.getSummaryResId()))
+ .setSummary(summary)
.setPendingIntent(pendingIntent)
.build();
}
@@ -1391,11 +1496,16 @@ final class SafetyCenterDataTracker {
private String getSafetyCenterStatusTitle(
@SafetyCenterStatus.OverallSeverityLevel int overallSeverityLevel,
@SafetyCenterStatus.RefreshStatus int refreshStatus,
+ @NonNull Set<Integer> allCurrentIssueCategories,
boolean hasSettingsToReview) {
String refreshStatusTitle = getSafetyCenterRefreshStatusTitle(refreshStatus);
if (refreshStatusTitle != null) {
return refreshStatusTitle;
}
+ boolean onlyAccountIssuesPresent =
+ allCurrentIssueCategories.size() == 1
+ && allCurrentIssueCategories.contains(
+ SafetySourceIssue.ISSUE_CATEGORY_ACCOUNT);
switch (overallSeverityLevel) {
case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN:
case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK:
@@ -1406,9 +1516,17 @@ final class SafetyCenterDataTracker {
return mSafetyCenterResourcesContext.getStringByName(
"overall_severity_level_ok_title");
case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION:
+ if (onlyAccountIssuesPresent) {
+ return mSafetyCenterResourcesContext.getStringByName(
+ "overall_severity_level_account_recommendation_title");
+ }
return mSafetyCenterResourcesContext.getStringByName(
"overall_severity_level_recommendation_title");
case SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING:
+ if (onlyAccountIssuesPresent) {
+ return mSafetyCenterResourcesContext.getStringByName(
+ "overall_severity_level_critical_account_warning_title");
+ }
return mSafetyCenterResourcesContext.getStringByName(
"overall_severity_level_critical_warning_title");
}
@@ -1521,4 +1639,12 @@ final class SafetyCenterDataTracker {
mDismissedAt = dismissedAt;
}
}
+
+ /**
+ * An internal mutable class to keep track of the overall {@link SafetyCenterStatus} error
+ * state; i.e. whether there is at least one user-visible entry that is showing an error.
+ */
+ private static final class SafetyCenterOverallStatusErrorState {
+ private boolean mHasAtLeastOneUserVisibleError = false;
+ }
}
diff --git a/service/java/com/android/safetycenter/SafetyCenterFlags.java b/service/java/com/android/safetycenter/SafetyCenterFlags.java
new file mode 100644
index 000000000..2c81ba528
--- /dev/null
+++ b/service/java/com/android/safetycenter/SafetyCenterFlags.java
@@ -0,0 +1,146 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.os.Binder;
+import android.provider.DeviceConfig;
+import android.util.ArraySet;
+
+import androidx.annotation.RequiresApi;
+
+import java.time.Duration;
+
+/** A class to access the Safety Center {@link DeviceConfig} flags. */
+@RequiresApi(TIRAMISU)
+final class SafetyCenterFlags {
+
+ /** {@link DeviceConfig} property name for {@link #getSafetyCenterEnabled()}. */
+ static final String PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled";
+
+ private static final String PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT =
+ "show_error_entries_on_timeout";
+
+ private static final String PROPERTY_REFRESH_SOURCE_TIMEOUT_MILLIS =
+ "safety_center_refresh_source_timeout_millis";
+
+ 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_UNTRACKED_SOURCES = "safety_center_untracked_sources";
+
+ private static final Duration REFRESH_SOURCE_TIMEOUT_DEFAULT_DURATION = Duration.ofSeconds(10);
+
+ private static final Duration RESOLVING_ACTION_TIMEOUT_DEFAULT_DURATION =
+ Duration.ofSeconds(10);
+
+ private static final Duration FGS_ALLOWLIST_DEFAULT_DURATION = Duration.ofSeconds(20);
+
+ /** Returns whether Safety Center is enabled. */
+ static boolean getSafetyCenterEnabled() {
+ return getBoolean(PROPERTY_SAFETY_CENTER_ENABLED, false);
+ }
+
+ /**
+ * 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, false);
+ }
+
+ /**
+ * 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.
+ */
+ static Duration getRefreshTimeout() {
+ return getDuration(
+ PROPERTY_REFRESH_SOURCE_TIMEOUT_MILLIS, REFRESH_SOURCE_TIMEOUT_DEFAULT_DURATION);
+ }
+
+ /**
+ * 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.
+ */
+ @NonNull
+ static ArraySet<String> getUntrackedSourceIds() {
+ String untrackedSourcesConfigString = getString(PROPERTY_UNTRACKED_SOURCES, "");
+ String[] untrackedSourcesList = untrackedSourcesConfigString.split(",");
+ return new ArraySet<>(untrackedSourcesList);
+ }
+
+ @NonNull
+ private static Duration getDuration(@NonNull String property, @NonNull Duration defaultValue) {
+ return Duration.ofMillis(getLong(property, defaultValue.toMillis()));
+ }
+
+ private static boolean getBoolean(@NonNull 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(@NonNull 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);
+ }
+ }
+
+ @NonNull
+ private static String getString(@NonNull String property, @NonNull 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);
+ }
+ }
+
+ private SafetyCenterFlags() {}
+}
diff --git a/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java b/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java
index c7a488400..77efe9535 100644
--- a/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java
+++ b/service/java/com/android/safetycenter/SafetyCenterRefreshTracker.java
@@ -22,8 +22,6 @@ import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_RESCAN_BUT
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
-import android.os.Binder;
-import android.provider.DeviceConfig;
import android.safetycenter.SafetyCenterManager.RefreshReason;
import android.safetycenter.SafetyCenterStatus;
import android.safetycenter.SafetyCenterStatus.RefreshStatus;
@@ -48,7 +46,6 @@ import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
final class SafetyCenterRefreshTracker {
private static final String TAG = "SafetyCenterRefreshTrac";
- private static final String PROPERTY_UNTRACKED_SOURCES = "safety_center_untracked_sources";
@NonNull private final SafetyCenterConfigReader mSafetyCenterConfigReader;
@@ -93,7 +90,7 @@ final class SafetyCenterRefreshTracker {
refreshBroadcastId,
refreshReason,
userProfileGroup,
- getUntrackedSourceIds());
+ SafetyCenterFlags.getUntrackedSourceIds());
for (int i = 0; i < broadcasts.size(); i++) {
Broadcast broadcast = broadcasts.get(i);
@@ -176,21 +173,27 @@ final class SafetyCenterRefreshTracker {
}
/**
- * Clears the refresh in progress with the given id, and returns whether it was ongoing.
+ * Clears the refresh in progress with the given id, and returns the {@link SafetySourceKey}s
+ * that were still in-flight prior to doing that, if any.
+ *
+ * <p>Returns {@code null} if there was no refresh in progress with the given {@code
+ * refreshBroadcastId}.
*
* <p>Note that this method simply clears the tracking of a refresh, and does not prevent
* scheduled broadcasts being sent by {@link
* android.safetycenter.SafetyCenterManager#refreshSafetySources}.
*/
// TODO(b/229188900): Should we stop any scheduled broadcasts from going out?
- boolean clearRefresh(@NonNull String refreshBroadcastId) {
+ @Nullable
+ ArraySet<SafetySourceKey> clearRefresh(@NonNull String refreshBroadcastId) {
if (!checkMethodValid("clearRefresh", refreshBroadcastId)) {
- return false;
+ return null;
}
Log.v(TAG, "Clearing refresh with refreshBroadcastId:" + refreshBroadcastId);
+ ArraySet<SafetySourceKey> stillInFlight = mRefreshInProgress.getSourceRefreshInFlight();
mRefreshInProgress = null;
- return true;
+ return stillInFlight;
}
/**
@@ -231,27 +234,6 @@ final class SafetyCenterRefreshTracker {
return true;
}
- /**
- * Returns the IDs of sources that should not be tracked, for example because they are
- * mid-rollout. Broadcasts are still sent to these sources.
- */
- private static ArraySet<String> getUntrackedSourceIds() {
- String untrackedSourcesConfigString = getUntrackedSourcesConfigString();
- String[] untrackedSourcesList = untrackedSourcesConfigString.split(",");
- return new ArraySet<>(untrackedSourcesList);
- }
-
- private static String getUntrackedSourcesConfigString() {
- // This call requires the READ_DEVICE_CONFIG permission.
- final long callingId = Binder.clearCallingIdentity();
- try {
- return DeviceConfig.getString(
- DeviceConfig.NAMESPACE_PRIVACY, PROPERTY_UNTRACKED_SOURCES, "");
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
/** Class representing the state of a refresh in progress. */
private static final class RefreshInProgress {
@NonNull private final String mId;
@@ -279,22 +261,28 @@ final class SafetyCenterRefreshTracker {
* in the refresh.
*/
@NonNull
- String getId() {
+ private String getId() {
return mId;
}
/** Returns the {@link RefreshReason} that was given for this {@link RefreshInProgress}. */
@RefreshReason
- int getReason() {
+ private int getReason() {
return mReason;
}
/** Returns the {@link UserProfileGroup} for which there is a {@link RefreshInProgress}. */
@NonNull
- UserProfileGroup getUserProfileGroup() {
+ private UserProfileGroup getUserProfileGroup() {
return mUserProfileGroup;
}
+ /** Returns the {@link SafetySourceKey} that are in-flight. */
+ @NonNull
+ private ArraySet<SafetySourceKey> getSourceRefreshInFlight() {
+ return mSourceRefreshInFlight;
+ }
+
private void addSourceRefreshInFlight(@NonNull SafetySourceKey safetySourceKey) {
boolean tracked = isTracked(safetySourceKey);
if (tracked) {
diff --git a/service/java/com/android/safetycenter/SafetyCenterService.java b/service/java/com/android/safetycenter/SafetyCenterService.java
index 2cbde0262..2cc953c75 100644
--- a/service/java/com/android/safetycenter/SafetyCenterService.java
+++ b/service/java/com/android/safetycenter/SafetyCenterService.java
@@ -23,6 +23,8 @@ import static android.os.Build.VERSION_CODES.TIRAMISU;
import static android.safetycenter.SafetyCenterManager.RefreshReason;
import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_FAILED;
+import static com.android.safetycenter.SafetyCenterFlags.PROPERTY_SAFETY_CENTER_ENABLED;
+
import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
@@ -54,6 +56,7 @@ import android.safetycenter.SafetySourceErrorDetails;
import android.safetycenter.SafetySourceIssue;
import android.safetycenter.config.SafetyCenterConfig;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.Keep;
@@ -95,37 +98,6 @@ public final class SafetyCenterService extends SystemService {
private static final String TAG = "SafetyCenterService";
- /** Phenotype flag that determines whether SafetyCenter is enabled. */
- private static final String PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled";
-
- /**
- * Device Config flag that determines 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 finished.
- */
- private static final String PROPERTY_REFRESH_SOURCE_TIMEOUT_MILLIS =
- "safety_center_refresh_source_timeout_millis";
-
- /**
- * Default 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 finished.
- */
- private static final Duration REFRESH_SOURCE_TIMEOUT_DEFAULT_DURATION = Duration.ofSeconds(10);
-
- /**
- * Device Config flag that determines the time for which Safety Center will wait for a source to
- * respond to a resolving action before timing out.
- */
- private static final String PROPERTY_RESOLVING_ACTION_TIMEOUT_MILLIS =
- "safety_center_resolve_action_timeout_millis";
-
- /**
- * Default time for which Safety Center will wait for a source to respond to a resolving action
- * before timing out.
- */
- private static final Duration RESOLVING_ACTION_TIMEOUT_DEFAULT_DURATION =
- Duration.ofSeconds(10);
-
/** The APEX name used to retrieve the APEX owned data directories. */
private static final String APEX_MODULE_NAME = "com.android.permission";
@@ -333,13 +305,14 @@ public final class SafetyCenterService extends SystemService {
synchronized (mApiLock) {
broadcasts = mSafetyCenterConfigReader.getBroadcasts();
+ mSafetyCenterDataTracker.clearSafetySourceErrors();
refreshBroadcastId =
mSafetyCenterRefreshTracker.reportRefreshInProgress(
refreshReason, userProfileGroup);
RefreshTimeout refreshTimeout =
new RefreshTimeout(refreshBroadcastId, userProfileGroup);
- mSafetyCenterTimeouts.add(refreshTimeout, getRefreshTimeout());
+ mSafetyCenterTimeouts.add(refreshTimeout, SafetyCenterFlags.getRefreshTimeout());
mSafetyCenterListeners.deliverUpdateForUserProfileGroup(
userProfileGroup, true, null);
@@ -559,7 +532,8 @@ public final class SafetyCenterService extends SystemService {
safetyCenterIssueActionId);
ResolvingActionTimeout resolvingActionTimeout =
new ResolvingActionTimeout(safetyCenterIssueActionId, userProfileGroup);
- mSafetyCenterTimeouts.add(resolvingActionTimeout, getResolvingActionTimeout());
+ mSafetyCenterTimeouts.add(
+ resolvingActionTimeout, SafetyCenterFlags.getResolvingActionTimeout());
mSafetyCenterListeners.deliverUpdateForUserProfileGroup(
userProfileGroup, true, null);
}
@@ -621,7 +595,7 @@ public final class SafetyCenterService extends SystemService {
}
private boolean isApiEnabled() {
- return canUseSafetyCenter() && getSafetyCenterEnabledProperty();
+ return canUseSafetyCenter() && SafetyCenterFlags.getSafetyCenterEnabled();
}
private void enforceAnyCallingOrSelfPermissions(
@@ -693,12 +667,14 @@ public final class SafetyCenterService extends SystemService {
}
/**
- * An {@link OnPropertiesChangedListener} for {@link #PROPERTY_SAFETY_CENTER_ENABLED} that sends
- * broadcasts when the SafetyCenter property is enabled or disabled.
+ * An {@link OnPropertiesChangedListener} for {@link
+ * SafetyCenterFlags#PROPERTY_SAFETY_CENTER_ENABLED} that sends broadcasts when the SafetyCenter
+ * property is enabled or disabled.
*
- * <p>This listener assumes that the {@link #PROPERTY_SAFETY_CENTER_ENABLED} value maps to
- * {@link SafetyCenterManager#isSafetyCenterEnabled()}. It should only be registered if the
- * device supports SafetyCenter and the {@link SafetyCenterConfig} was loaded successfully.
+ * <p>This listener assumes that the {@link SafetyCenterFlags#PROPERTY_SAFETY_CENTER_ENABLED}
+ * value maps to {@link SafetyCenterManager#isSafetyCenterEnabled()}. It should only be
+ * registered if the device supports SafetyCenter and the {@link SafetyCenterConfig} was loaded
+ * successfully.
*
* <p>This listener is not thread-safe; it should be called on a single thread.
*/
@@ -721,7 +697,7 @@ public final class SafetyCenterService extends SystemService {
}
private void setInitialState() {
- mSafetyCenterEnabled = getSafetyCenterEnabledProperty();
+ mSafetyCenterEnabled = SafetyCenterFlags.getSafetyCenterEnabled();
}
private void onSafetyCenterEnabledChanged(boolean safetyCenterEnabled) {
@@ -770,16 +746,26 @@ public final class SafetyCenterService extends SystemService {
public void run() {
synchronized (mApiLock) {
mSafetyCenterTimeouts.remove(this);
- boolean hasClearedRefresh =
+ ArraySet<SafetySourceKey> stillInFlight =
mSafetyCenterRefreshTracker.clearRefresh(mRefreshBroadcastId);
- if (!hasClearedRefresh) {
+ if (stillInFlight == null) {
return;
}
+ boolean showErrorEntriesOnTimeout =
+ SafetyCenterFlags.getShowErrorEntriesOnTimeout();
+ if (showErrorEntriesOnTimeout) {
+ for (int i = 0; i < stillInFlight.size(); i++) {
+ mSafetyCenterDataTracker.setSafetySourceError(stillInFlight.valueAt(i));
+ }
+ }
mSafetyCenterListeners.deliverUpdateForUserProfileGroup(
mUserProfileGroup,
true,
- new SafetyCenterErrorDetails(
- mSafetyCenterResourcesContext.getStringByName("refresh_timeout")));
+ showErrorEntriesOnTimeout
+ ? null
+ : new SafetyCenterErrorDetails(
+ mSafetyCenterResourcesContext.getStringByName(
+ "refresh_timeout")));
}
Log.v(
@@ -865,38 +851,6 @@ public final class SafetyCenterService extends SystemService {
return mDeviceSupportsSafetyCenter && mConfigAvailable;
}
- private boolean getSafetyCenterEnabledProperty() {
- // This call requires the READ_DEVICE_CONFIG permission.
- final long callingId = Binder.clearCallingIdentity();
- try {
- return DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_PRIVACY,
- PROPERTY_SAFETY_CENTER_ENABLED,
- /* defaultValue = */ false);
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
- @GuardedBy("mApiLock")
- private void clearDataLocked() {
- mSafetyCenterDataTracker.clear();
- mSafetyCenterTimeouts.clear();
- mSafetyCenterRefreshTracker.clearRefresh();
- scheduleWriteSafetyCenterIssueCacheFileIfNeededLocked();
- }
-
- private void onRemoveUser(@UserIdInt int userId) {
- UserProfileGroup userProfileGroup = UserProfileGroup.from(getContext(), userId);
- synchronized (mApiLock) {
- mSafetyCenterDataTracker.clearForUser(userId);
- mSafetyCenterListeners.clearForUser(userId);
- mSafetyCenterRefreshTracker.clearRefreshForUser(userId);
- mSafetyCenterListeners.deliverUpdateForUserProfileGroup(userProfileGroup, true, null);
- scheduleWriteSafetyCenterIssueCacheFileIfNeededLocked();
- }
- }
-
private void registerUserRemovedReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_REMOVED);
@@ -921,39 +875,14 @@ public final class SafetyCenterService extends SystemService {
null);
}
- /**
- * 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 finished.
- */
- private Duration getRefreshTimeout() {
- // This call requires the READ_DEVICE_CONFIG permission.
- final long callingId = Binder.clearCallingIdentity();
- try {
- return Duration.ofMillis(
- DeviceConfig.getLong(
- DeviceConfig.NAMESPACE_PRIVACY,
- PROPERTY_REFRESH_SOURCE_TIMEOUT_MILLIS,
- REFRESH_SOURCE_TIMEOUT_DEFAULT_DURATION.toMillis()));
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- }
-
- /**
- * Returns the time for which Safety Center will wait for a source to respond to a resolving
- * action before timing out.
- */
- private Duration getResolvingActionTimeout() {
- // This call requires the READ_DEVICE_CONFIG permission.
- final long callingId = Binder.clearCallingIdentity();
- try {
- return Duration.ofMillis(
- DeviceConfig.getLong(
- DeviceConfig.NAMESPACE_PRIVACY,
- PROPERTY_RESOLVING_ACTION_TIMEOUT_MILLIS,
- RESOLVING_ACTION_TIMEOUT_DEFAULT_DURATION.toMillis()));
- } finally {
- Binder.restoreCallingIdentity(callingId);
+ private void onRemoveUser(@UserIdInt int userId) {
+ UserProfileGroup userProfileGroup = UserProfileGroup.from(getContext(), userId);
+ synchronized (mApiLock) {
+ mSafetyCenterDataTracker.clearForUser(userId);
+ mSafetyCenterListeners.clearForUser(userId);
+ mSafetyCenterRefreshTracker.clearRefreshForUser(userId);
+ mSafetyCenterListeners.deliverUpdateForUserProfileGroup(userProfileGroup, true, null);
+ scheduleWriteSafetyCenterIssueCacheFileIfNeededLocked();
}
}
@@ -1008,4 +937,12 @@ public final class SafetyCenterService extends SystemService {
// It should resolve to /data/misc/apexdata/com.android.permission/safety_center_issues.xml
return new File(dataDirectory, SAFETY_CENTER_ISSUES_CACHE_FILE_NAME);
}
+
+ @GuardedBy("mApiLock")
+ private void clearDataLocked() {
+ mSafetyCenterDataTracker.clear();
+ mSafetyCenterTimeouts.clear();
+ mSafetyCenterRefreshTracker.clearRefresh();
+ scheduleWriteSafetyCenterIssueCacheFileIfNeededLocked();
+ }
}
diff --git a/service/java/com/android/safetycenter/SafetySources.java b/service/java/com/android/safetycenter/SafetySources.java
index 97686826a..d6fc3cfab 100644
--- a/service/java/com/android/safetycenter/SafetySources.java
+++ b/service/java/com/android/safetycenter/SafetySources.java
@@ -91,4 +91,6 @@ final class SafetySources {
Log.w(TAG, "Unexpected safety source type: " + safetySourceType);
return false;
}
+
+ private SafetySources() {}
}
diff --git a/service/java/com/android/safetycenter/UserProfileGroup.java b/service/java/com/android/safetycenter/UserProfileGroup.java
index c47cd40fa..609db0584 100644
--- a/service/java/com/android/safetycenter/UserProfileGroup.java
+++ b/service/java/com/android/safetycenter/UserProfileGroup.java
@@ -204,6 +204,16 @@ final class UserProfileGroup {
return false;
}
+ /** Returns whether the given {@code userId} is associated with a running managed profile. */
+ boolean isManagedUserRunning(@UserIdInt int userId) {
+ for (int i = 0; i < mManagedRunningProfilesUserIds.length; i++) {
+ if (userId == mManagedRunningProfilesUserIds[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterActivityTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterActivityTest.kt
index 71e9a629b..8ef02cc37 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterActivityTest.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterActivityTest.kt
@@ -77,7 +77,7 @@ class SafetyCenterActivityTest {
if (!shouldRunTests) {
return
}
- safetyCenterCtsHelper.setEnabled(true)
+ safetyCenterCtsHelper.setup()
}
@After
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagedDeviceTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagedDeviceTest.kt
index c737cf749..2f4eb82ef 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagedDeviceTest.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagedDeviceTest.kt
@@ -16,13 +16,16 @@
package android.safetycenter.cts
+import android.Manifest.permission.INTERACT_ACROSS_USERS
import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
-import android.Manifest.permission.MODIFY_QUIET_MODE
-import android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE
import android.content.Context
-import android.os.UserHandle
-import android.os.UserManager
import android.safetycenter.SafetyCenterData
+import android.safetycenter.SafetyCenterEntry
+import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK
+import android.safetycenter.SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNKNOWN
+import android.safetycenter.SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON
+import android.safetycenter.SafetyCenterEntryGroup
+import android.safetycenter.SafetyCenterEntryOrGroup
import android.safetycenter.SafetyCenterManager
import android.safetycenter.SafetyCenterStaticEntry
import android.safetycenter.SafetyCenterStaticEntryGroup
@@ -31,6 +34,7 @@ import android.safetycenter.SafetySourceData
import android.safetycenter.cts.testing.SafetyCenterActivityLauncher.launchSafetyCenterActivity
import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.getSafetyCenterDataWithPermission
import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.getSafetySourceDataWithPermission
+import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.setSafetySourceDataWithPermission
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.DYNAMIC_ALL_PROFILE_SAFETY_SOURCE
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.ISSUE_ONLY_ALL_OPTIONAL_ID
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.ISSUE_ONLY_ALL_PROFILE_SOURCE_ID
@@ -39,9 +43,11 @@ import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.ISSUE_ONLY_SOURCE
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.SINGLE_SOURCE_ALL_PROFILE_CONFIG
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.SINGLE_SOURCE_ALL_PROFILE_ID
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.SINGLE_SOURCE_CONFIG
+import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.SINGLE_SOURCE_GROUP_ID
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.SINGLE_SOURCE_ID
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.STATIC_ALL_PROFILE_SOURCES_CONFIG
import android.safetycenter.cts.testing.SafetyCenterCtsConfigs.getWorkPolicyInfoConfig
+import android.safetycenter.cts.testing.SafetyCenterCtsData
import android.safetycenter.cts.testing.SafetyCenterCtsHelper
import android.safetycenter.cts.testing.SafetyCenterFlags.deviceSupportsSafetyCenter
import android.safetycenter.cts.testing.SafetySourceCtsData
@@ -52,9 +58,9 @@ import android.safetycenter.cts.testing.UiTestHelper.waitTextNotDisplayed
import androidx.test.core.app.ApplicationProvider
import com.android.bedstead.harrier.BedsteadJUnit4
import com.android.bedstead.harrier.DeviceState
+import com.android.bedstead.harrier.OptionalBoolean.TRUE
import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile
import com.android.bedstead.harrier.annotations.enterprise.EnsureHasDeviceOwner
-import com.android.bedstead.nene.TestApis
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertFailsWith
import org.junit.After
@@ -70,7 +76,7 @@ import org.junit.runner.RunWith
@Ignore
@RunWith(BedsteadJUnit4::class)
// TODO(b/234108780): Enable these back when we figure a way to make sure they don't fail due to
-// timeouts with Bedstead.
+// timeouts with Bedstead. Consider marking them as running only in post-submit in the meantime.
class SafetyCenterManagedDeviceTest {
companion object {
@@ -81,10 +87,39 @@ class SafetyCenterManagedDeviceTest {
private val safetyCenterCtsHelper = SafetyCenterCtsHelper(context)
private val safetySourceCtsData = SafetySourceCtsData(context)
private val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)!!
- private val userManager = context.getSystemService(UserManager::class.java)!!
+
// JUnit's Assume is not supported in @BeforeClass by the CTS tests runner, so this is used to
// manually skip the setup and teardown methods.
private val shouldRunTests = context.deviceSupportsSafetyCenter()
+ private var inQuietMode = false
+
+ private val safetyCenterStatusOk =
+ SafetyCenterStatus.Builder("Looks good", "This device is protected")
+ .setSeverityLevel(SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK)
+ .build()
+ private val staticEntry =
+ SafetyCenterStaticEntry.Builder("OK")
+ .setPendingIntent(safetySourceCtsData.redirectPendingIntent)
+ .setSummary("OK")
+ .build()
+ private val staticEntryForWork
+ get() =
+ SafetyCenterStaticEntry.Builder("Attention")
+ .setSummary("OK")
+ .setPendingIntent(redirectPendingIntentForWork)
+ .build()
+ private val staticEntryForWorkQuietMode
+ get() =
+ SafetyCenterStaticEntry.Builder("Attention")
+ .setSummary("Work profile is paused")
+ .setPendingIntent(redirectPendingIntentForWork)
+ .build()
+
+ private val redirectPendingIntentForWork
+ get() =
+ callWithShellPermissionIdentity(
+ { SafetySourceCtsData.createRedirectPendingIntent(getManagedContext()) },
+ INTERACT_ACROSS_USERS)
@Before
fun assumeDeviceSupportsSafetyCenterToRunTests() {
@@ -96,7 +131,7 @@ class SafetyCenterManagedDeviceTest {
if (!shouldRunTests) {
return
}
- safetyCenterCtsHelper.setEnabled(true)
+ safetyCenterCtsHelper.setup()
}
@After
@@ -105,6 +140,7 @@ class SafetyCenterManagedDeviceTest {
return
}
safetyCenterCtsHelper.reset()
+ resetQuietMode()
}
@Test
@@ -125,33 +161,28 @@ class SafetyCenterManagedDeviceTest {
@Test
@EnsureHasWorkProfile
- @Ignore
fun launchActivity_withQuietModeEnabled_shouldNotDisplayWorkPolicyInfo() {
- val managedUserId = getManagedProfileUserId()
- val profileHandle: UserHandle = UserHandle.of(managedUserId)
safetyCenterCtsHelper.setConfig(context.getWorkPolicyInfoConfig())
findWorkPolicyInfo()
- callWithShellPermissionIdentity(
- { userManager.requestQuietModeEnabled(true, profileHandle) }, MODIFY_QUIET_MODE)
-
+ setQuietMode(true)
context.launchSafetyCenterActivity { waitTextNotDisplayed("Your work policy info") }
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun launchActivity_sourceWithWorkProfile_showBothEntriesWithDefaultInformation() {
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_ALL_PROFILE_CONFIG)
+
val titleResId = DYNAMIC_ALL_PROFILE_SAFETY_SOURCE.titleResId
val titleForWorkResId = DYNAMIC_ALL_PROFILE_SAFETY_SOURCE.titleForWorkResId
-
context.launchSafetyCenterActivity {
findAllText(context.getString(titleResId), context.getString(titleForWorkResId))
}
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun getSafetySourceData_withoutInteractAcrossUserPermission_shouldThrowError() {
val managedSafetyCenterManager = getManagedSafetyCenterManager()
val setData = safetySourceCtsData.information
@@ -168,43 +199,28 @@ class SafetyCenterManagedDeviceTest {
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun getSafetyCenterData_staticSourceWithWorkProfile_shouldBeAbleToGetData() {
- val safetyCenterStatusOk =
- SafetyCenterStatus.Builder("Looks good", "This device is protected")
- .setSeverityLevel(SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK)
- .build()
- val staticEntry =
- SafetyCenterStaticEntry.Builder("OK")
- .setPendingIntent(safetySourceCtsData.redirectPendingIntent)
- .setSummary("OK")
- .build()
- val staticEntryForWork =
- SafetyCenterStaticEntry.Builder("Attention")
- .setSummary("OK")
- .setPendingIntent(
- SafetySourceCtsData.createRedirectPendingIntent(getManagedContext()))
- .build()
+ safetyCenterCtsHelper.setConfig(STATIC_ALL_PROFILE_SOURCES_CONFIG)
+
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+
val safetyCenterStaticData =
SafetyCenterData(
safetyCenterStatusOk,
emptyList(),
emptyList(),
listOf(SafetyCenterStaticEntryGroup("OK", listOf(staticEntry, staticEntryForWork))))
-
- safetyCenterCtsHelper.setConfig(STATIC_ALL_PROFILE_SOURCES_CONFIG)
- val apiSafetySourceData = safetyCenterManager.getSafetyCenterDataWithPermission()
-
- assertThat(apiSafetySourceData).isEqualTo(safetyCenterStaticData)
+ assertThat(apiSafetyCenterData).isEqualTo(safetyCenterStaticData)
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun setSafetySourceData_primaryProfileIssueOnlySource_shouldNotBeAbleToSetDataToWorkProfile() {
- val managedSafetyCenterManager = getManagedSafetyCenterManager()
- val setDataForWork = safetySourceCtsData.informationForWork
safetyCenterCtsHelper.setConfig(ISSUE_ONLY_SOURCE_CONFIG)
+ val managedSafetyCenterManager = getManagedSafetyCenterManager()
+ val setDataForWork = safetySourceCtsData.informationForWork
assertFailsWith(IllegalArgumentException::class) {
managedSafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
ISSUE_ONLY_ALL_OPTIONAL_ID, setDataForWork)
@@ -212,14 +228,13 @@ class SafetyCenterManagedDeviceTest {
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun setSafetySourceData_withoutInteractAcrossUserPermission_shouldThrowError() {
- val managedSafetyCenterManager = getManagedSafetyCenterManager()
- val setData = safetySourceCtsData.information
- val setDataForWork = safetySourceCtsData.informationForWork
-
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_ALL_PROFILE_CONFIG)
+ val setData = safetySourceCtsData.information
safetyCenterCtsHelper.setData(SINGLE_SOURCE_ALL_PROFILE_ID, setData)
+ val managedSafetyCenterManager = getManagedSafetyCenterManager()
+ val setDataForWork = safetySourceCtsData.informationForWork
managedSafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
SINGLE_SOURCE_ALL_PROFILE_ID, setDataForWork)
@@ -230,7 +245,7 @@ class SafetyCenterManagedDeviceTest {
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun setSafetySourceData_issuesOnlySourceWithWorkProfile_shouldBeAbleToSetData() {
val managedSafetyCenterManager = getManagedSafetyCenterManager()
val dataToSet = SafetySourceCtsData.issuesOnly(safetySourceCtsData.recommendationIssue)
@@ -238,9 +253,9 @@ class SafetyCenterManagedDeviceTest {
SafetySourceCtsData.issuesOnly(safetySourceCtsData.criticalResolvingIssue)
safetyCenterCtsHelper.setConfig(ISSUE_ONLY_SOURCE_ALL_PROFILE_CONFIG)
safetyCenterCtsHelper.setData(ISSUE_ONLY_ALL_PROFILE_SOURCE_ID, dataToSet)
-
managedSafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
ISSUE_ONLY_ALL_PROFILE_SOURCE_ID, dataToSetForWork)
+
val apiSafetySourceData =
safetyCenterManager.getSafetySourceDataWithPermission(ISSUE_ONLY_ALL_PROFILE_SOURCE_ID)
val apiSafetySourceDataForWork =
@@ -252,12 +267,12 @@ class SafetyCenterManagedDeviceTest {
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun setSafetySourceData_primaryProfileSource_shouldNotBeAbleToSetDataToWorkProfile() {
- val managedSafetyCenterManager = getManagedSafetyCenterManager()
- val setDataForWork = safetySourceCtsData.informationForWork
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ val managedSafetyCenterManager = getManagedSafetyCenterManager()
+ val setDataForWork = safetySourceCtsData.informationForWork
assertFailsWith(IllegalArgumentException::class) {
managedSafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
SINGLE_SOURCE_ID, setDataForWork)
@@ -265,20 +280,17 @@ class SafetyCenterManagedDeviceTest {
}
@Test
- @EnsureHasWorkProfile
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
fun setSafetySourceData_sourceWithWorkProfile_bothEntriesShouldShowWhenQuietModeIsEnabled() {
val managedSafetyCenterManager = getManagedSafetyCenterManager()
val setData = safetySourceCtsData.information
val setDataForWork = safetySourceCtsData.informationForWork
- val managedUserId = getManagedProfileUserId()
- val profileHandle: UserHandle = UserHandle.of(managedUserId)
-
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_ALL_PROFILE_CONFIG)
safetyCenterCtsHelper.setData(SINGLE_SOURCE_ALL_PROFILE_ID, setData)
managedSafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
SINGLE_SOURCE_ALL_PROFILE_ID, setDataForWork)
- callWithShellPermissionIdentity(
- { userManager.requestQuietModeEnabled(true, profileHandle) }, MODIFY_QUIET_MODE)
+
+ setQuietMode(true)
val apiSafetySourceData =
safetyCenterManager.getSafetySourceDataWithPermission(SINGLE_SOURCE_ALL_PROFILE_ID)
val apiSafetySourceDataForWork =
@@ -290,16 +302,87 @@ class SafetyCenterManagedDeviceTest {
}
@Test
- @EnsureHasWorkProfile
- fun setSafetySourceData_sourceWithWorkProfile_shouldBeAbleToSetData() {
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
+ fun getSafetyCenterData_staticSourceWithQuietMode_shouldHaveWorkProfilePausedSummary() {
+ safetyCenterCtsHelper.setConfig(STATIC_ALL_PROFILE_SOURCES_CONFIG)
+
+ setQuietMode(true)
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+
+ val safetyCenterStaticData =
+ SafetyCenterData(
+ safetyCenterStatusOk,
+ emptyList(),
+ emptyList(),
+ listOf(
+ SafetyCenterStaticEntryGroup(
+ "OK", listOf(staticEntry, staticEntryForWorkQuietMode))))
+ assertThat(apiSafetyCenterData).isEqualTo(safetyCenterStaticData)
+ }
+
+ @Test
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
+ fun setSafetySourceData_quietModeEnabled_workEntryShouldBeDisabled() {
val managedSafetyCenterManager = getManagedSafetyCenterManager()
val setData = safetySourceCtsData.information
val setDataForWork = safetySourceCtsData.informationForWork
+ val managedUserId = deviceState.workProfile().id()
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_ALL_PROFILE_CONFIG)
safetyCenterCtsHelper.setData(SINGLE_SOURCE_ALL_PROFILE_ID, setData)
+ managedSafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
+ SINGLE_SOURCE_ALL_PROFILE_ID, setDataForWork)
+
+ setQuietMode(true)
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+ val entry =
+ SafetyCenterEntry.Builder(
+ SafetyCenterCtsData.entryId(SINGLE_SOURCE_ALL_PROFILE_ID), "Ok title")
+ .setSeverityLevel(ENTRY_SEVERITY_LEVEL_OK)
+ .setSummary("Ok summary")
+ .setPendingIntent(safetySourceCtsData.redirectPendingIntent)
+ .setSeverityUnspecifiedIconType(
+ SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
+ .build()
+ val entryForWork =
+ SafetyCenterEntry.Builder(
+ SafetyCenterCtsData.entryId(SINGLE_SOURCE_ALL_PROFILE_ID, managedUserId),
+ context.getString(DYNAMIC_ALL_PROFILE_SAFETY_SOURCE.titleForWorkResId))
+ .setSeverityLevel(ENTRY_SEVERITY_LEVEL_UNKNOWN)
+ .setSummary("Work profile is paused")
+ .setPendingIntent(redirectPendingIntentForWork)
+ .setSeverityUnspecifiedIconType(
+ SafetyCenterEntry.SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
+ .setEnabled(false)
+ .build()
+ val entryGroup =
+ SafetyCenterEntryGroup.Builder(
+ SafetyCenterCtsData.entryGroupId(SINGLE_SOURCE_GROUP_ID), "OK")
+ .setSeverityLevel(ENTRY_SEVERITY_LEVEL_OK)
+ .setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_ICON)
+ .setSummary("OK")
+ .setEntries(listOf(entry, entryForWork))
+ .build()
+ val safetyCenterData =
+ SafetyCenterData(
+ safetyCenterStatusOk,
+ emptyList(),
+ listOf(SafetyCenterEntryOrGroup(entryGroup)),
+ emptyList())
+ assertThat(apiSafetyCenterData).isEqualTo(safetyCenterData)
+ }
+
+ @Test
+ @EnsureHasWorkProfile(installInstrumentedApp = TRUE)
+ fun setSafetySourceData_sourceWithWorkProfile_shouldBeAbleToSetData() {
+ val managedSafetyCenterManager = getManagedSafetyCenterManager()
+ val setData = safetySourceCtsData.information
+ val setDataForWork = safetySourceCtsData.informationForWork
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_ALL_PROFILE_CONFIG)
+ safetyCenterCtsHelper.setData(SINGLE_SOURCE_ALL_PROFILE_ID, setData)
managedSafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
SINGLE_SOURCE_ALL_PROFILE_ID, setDataForWork)
+
val apiSafetySourceData =
safetyCenterManager.getSafetySourceDataWithPermission(SINGLE_SOURCE_ALL_PROFILE_ID)
val apiSafetySourceDataForWork =
@@ -323,25 +406,34 @@ class SafetyCenterManagedDeviceTest {
}
private fun getManagedContext(): Context {
- return TestApis.context().androidContextAsUser(deviceState.workProfile())
- }
-
- private fun getManagedProfileUserId(): Int {
- return deviceState.workProfile().id()
+ return callWithShellPermissionIdentity(
+ { context.createContextAsUser(deviceState.workProfile().userHandle(), 0) },
+ INTERACT_ACROSS_USERS_FULL)
}
private fun SafetyCenterManager.getSafetySourceDataWithPermissionForManagedUser(
id: String
): SafetySourceData? =
callWithShellPermissionIdentity(
- { getSafetySourceData(id) }, SEND_SAFETY_CENTER_UPDATE, INTERACT_ACROSS_USERS_FULL)
+ { getSafetySourceDataWithPermission(id) }, INTERACT_ACROSS_USERS_FULL)
private fun SafetyCenterManager.setSafetySourceDataWithPermissionForManagedUser(
id: String,
dataToSet: SafetySourceData
) =
callWithShellPermissionIdentity(
- { setSafetySourceData(id, dataToSet, EVENT_SOURCE_STATE_CHANGED) },
- SEND_SAFETY_CENTER_UPDATE,
+ { setSafetySourceDataWithPermission(id, dataToSet, EVENT_SOURCE_STATE_CHANGED) },
INTERACT_ACROSS_USERS_FULL)
+
+ private fun setQuietMode(value: Boolean) {
+ deviceState.workProfile().setQuietMode(value)
+ inQuietMode = value
+ }
+
+ private fun resetQuietMode() {
+ if (!inQuietMode) {
+ return
+ }
+ setQuietMode(false)
+ }
}
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
index 3b97b5121..df6c808b5 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterManagerTest.kt
@@ -118,6 +118,7 @@ import android.safetycenter.cts.testing.SafetySourceReceiver.Companion.refreshSa
import android.safetycenter.cts.testing.SafetySourceReceiver.Companion.refreshSafetySourcesWithoutReceiverPermissionAndWait
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.safetycenter.resources.SafetyCenterResourcesContext
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import kotlin.test.assertFailsWith
@@ -132,6 +133,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SafetyCenterManagerTest {
private val context: Context = getApplicationContext()
+ private val safetyCenterResourcesContext = SafetyCenterResourcesContext(context)
private val safetyCenterCtsHelper = SafetyCenterCtsHelper(context)
private val safetySourceCtsData = SafetySourceCtsData(context)
private val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)!!
@@ -141,6 +143,12 @@ class SafetyCenterManagerTest {
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_OK)
.build()
+ private val safetyCenterStatusOkScanning =
+ SafetyCenterStatus.Builder("Scanning", "Checking device status…")
+ .setSeverityLevel(OVERALL_SEVERITY_LEVEL_OK)
+ .setRefreshStatus(REFRESH_STATUS_FULL_RESCAN_IN_PROGRESS)
+ .build()
+
private val safetyCenterStatusOkOneAlert =
SafetyCenterStatus.Builder("Looks good", "1 alert")
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_OK)
@@ -161,11 +169,32 @@ class SafetyCenterManagerTest {
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_RECOMMENDATION)
.build()
+ private val safetyCenterStatusAccountRecommendationOneAlert =
+ SafetyCenterStatus.Builder(
+ safetyCenterResourcesContext.getStringByName(
+ "overall_severity_level_account_recommendation_title"),
+ "1 alert")
+ .setSeverityLevel(OVERALL_SEVERITY_LEVEL_RECOMMENDATION)
+ .build()
+
private val safetyCenterStatusCriticalOneAlert =
SafetyCenterStatus.Builder("Device is at risk", "1 alert")
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING)
.build()
+ private val safetyCenterStatusCriticalTwoAlerts =
+ SafetyCenterStatus.Builder("Device is at risk", "2 alerts")
+ .setSeverityLevel(OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING)
+ .build()
+
+ private val safetyCenterStatusAccountCriticalOneAlert =
+ SafetyCenterStatus.Builder(
+ safetyCenterResourcesContext.getStringByName(
+ "overall_severity_level_critical_account_warning_title"),
+ "1 alert")
+ .setSeverityLevel(OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING)
+ .build()
+
private val safetyCenterStatusCriticalSixAlerts =
SafetyCenterStatus.Builder("Device is at risk", "6 alerts")
.setSeverityLevel(OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING)
@@ -233,6 +262,15 @@ class SafetyCenterManagerTest {
.setPendingIntent(safetySourceCtsData.redirectPendingIntent)
.build()))
+ private val safetyCenterDataFromConfigScanning =
+ SafetyCenterData(
+ safetyCenterStatusOkScanning,
+ emptyList(),
+ listOf(
+ SafetyCenterEntryOrGroup(
+ safetyCenterEntryDefaultBuilder(SINGLE_SOURCE_ID).build())),
+ emptyList())
+
private val safetyCenterDataFromConfig =
SafetyCenterData(
safetyCenterStatusOk,
@@ -256,6 +294,13 @@ class SafetyCenterManagerTest {
listOf(SafetyCenterEntryOrGroup(safetyCenterEntryOk(SINGLE_SOURCE_ID))),
emptyList())
+ private val safetyCenterDataOkReviewError =
+ SafetyCenterData(
+ safetyCenterStatusOkReview,
+ emptyList(),
+ listOf(SafetyCenterEntryOrGroup(safetyCenterEntryError(SINGLE_SOURCE_ID))),
+ emptyList())
+
private val safetyCenterDataOkOneAlert =
SafetyCenterData(
safetyCenterStatusOkOneAlert,
@@ -291,6 +336,13 @@ class SafetyCenterManagerTest {
listOf(SafetyCenterEntryOrGroup(safetyCenterEntryRecommendation(SINGLE_SOURCE_ID))),
emptyList())
+ private val safetyCenterDataAccountRecommendationOneAlert =
+ SafetyCenterData(
+ safetyCenterStatusAccountRecommendationOneAlert,
+ listOf(safetyCenterIssueRecommendation(SINGLE_SOURCE_ID)),
+ listOf(SafetyCenterEntryOrGroup(safetyCenterEntryRecommendation(SINGLE_SOURCE_ID))),
+ emptyList())
+
private val safetyCenterDataCriticalOneAlert =
SafetyCenterData(
safetyCenterStatusCriticalOneAlert,
@@ -298,6 +350,22 @@ class SafetyCenterManagerTest {
listOf(safetyCenterEntryOrGroupCritical),
emptyList())
+ private val safetyCenterDataAccountCriticalOneAlert =
+ SafetyCenterData(
+ safetyCenterStatusAccountCriticalOneAlert,
+ listOf(safetyCenterIssueCritical(SINGLE_SOURCE_ID)),
+ listOf(safetyCenterEntryOrGroupCritical),
+ emptyList())
+
+ private val safetyCenterDataCriticalTwoAlerts =
+ SafetyCenterData(
+ safetyCenterStatusCriticalTwoAlerts,
+ listOf(
+ safetyCenterIssueCritical(SINGLE_SOURCE_ID),
+ safetyCenterIssueRecommendation(SINGLE_SOURCE_ID)),
+ listOf(safetyCenterEntryOrGroupCritical),
+ emptyList())
+
private val safetyCenterDataCriticalOneAlertInFlight =
SafetyCenterData(
safetyCenterStatusCriticalOneAlert,
@@ -389,7 +457,7 @@ class SafetyCenterManagerTest {
if (!shouldRunTests) {
return
}
- safetyCenterCtsHelper.setEnabled(true)
+ safetyCenterCtsHelper.setup()
}
@After
@@ -811,6 +879,21 @@ class SafetyCenterManagerTest {
}
@Test
+ fun reportSafetySourceError_notifiesErrorEntryButDoesntCallErrorListener() {
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ val listener = safetyCenterCtsHelper.addListener()
+
+ safetyCenterManager.reportSafetySourceErrorWithPermission(
+ SINGLE_SOURCE_ID, SafetySourceErrorDetails(EVENT_SOURCE_STATE_CHANGED))
+
+ val safetyCenterDataFromListener = listener.receiveSafetyCenterData()
+ assertThat(safetyCenterDataFromListener).isEqualTo(safetyCenterDataOkReviewError)
+ assertFailsWith(TimeoutCancellationException::class) {
+ listener.receiveSafetyCenterErrorDetails(TIMEOUT_SHORT)
+ }
+ }
+
+ @Test
fun reportSafetySourceError_unknownId_throwsIllegalArgumentException() {
val thrown =
assertFailsWith(IllegalArgumentException::class) {
@@ -854,13 +937,17 @@ class SafetyCenterManagerTest {
}
@Test
- fun reportSafetySourceError_withFlagDisabled_doesntCheckRequest() {
+ fun reportSafetySourceError_withFlagDisabled_doesntCallListener() {
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
val listener = safetyCenterCtsHelper.addListener()
safetyCenterCtsHelper.setEnabled(false)
safetyCenterManager.reportSafetySourceErrorWithPermission(
- "unknown_id", SafetySourceErrorDetails(EVENT_SOURCE_STATE_CHANGED))
+ SINGLE_SOURCE_ID, SafetySourceErrorDetails(EVENT_SOURCE_STATE_CHANGED))
+
+ assertFailsWith(TimeoutCancellationException::class) {
+ listener.receiveSafetyCenterData(TIMEOUT_SHORT)
+ }
}
@Test
@@ -1201,8 +1288,8 @@ class SafetyCenterManagerTest {
safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
REFRESH_REASON_RESCAN_BUTTON_CLICK)
- val safetyCenterErrorDetailsFromListener = listener.receiveSafetyCenterErrorDetails()
+ val safetyCenterErrorDetailsFromListener = listener.receiveSafetyCenterErrorDetails()
assertThat(safetyCenterErrorDetailsFromListener)
.isEqualTo(SafetyCenterErrorDetails("Couldn’t refresh status"))
}
@@ -1250,20 +1337,62 @@ class SafetyCenterManagerTest {
@Test
fun refreshSafetySources_withEmptyUntrackedSourceConfigAndSourceThatTimesOut_timesOut() {
SafetyCenterFlags.refreshTimeout = TIMEOUT_SHORT
- SafetyCenterFlags.untrackedSources = emptySet()
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
// SINGLE_SOURCE_ID will timeout
val listener = safetyCenterCtsHelper.addListener()
safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
REFRESH_REASON_RESCAN_BUTTON_CLICK)
- val safetyCenterErrorDetailsFromListener = listener.receiveSafetyCenterErrorDetails()
+ val safetyCenterErrorDetailsFromListener = listener.receiveSafetyCenterErrorDetails()
assertThat(safetyCenterErrorDetailsFromListener)
.isEqualTo(SafetyCenterErrorDetails("Couldn’t refresh status"))
}
@Test
+ fun refreshSafetySources_withShowEntriesOnTimeout_marksSafetySourceAsError() {
+ SafetyCenterFlags.refreshTimeout = TIMEOUT_SHORT
+ SafetyCenterFlags.showErrorEntriesOnTimeout = true
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ val listener = safetyCenterCtsHelper.addListener()
+
+ safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
+ REFRESH_REASON_RESCAN_BUTTON_CLICK)
+
+ val safetyCenterBeforeTimeout = listener.receiveSafetyCenterData()
+ assertThat(safetyCenterBeforeTimeout).isEqualTo(safetyCenterDataFromConfigScanning)
+ val safetyCenterDataAfterTimeout = listener.receiveSafetyCenterData()
+ assertThat(safetyCenterDataAfterTimeout).isEqualTo(safetyCenterDataOkReviewError)
+ assertFailsWith(TimeoutCancellationException::class) {
+ listener.receiveSafetyCenterErrorDetails(TIMEOUT_SHORT)
+ }
+ }
+
+ @Test
+ fun refreshSafetySources_withShowEntriesOnTimeout_stopsShowingErrorWhenTryingAgain() {
+ SafetyCenterFlags.refreshTimeout = TIMEOUT_SHORT
+ SafetyCenterFlags.showErrorEntriesOnTimeout = true
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ val listener = safetyCenterCtsHelper.addListener()
+ safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
+ REFRESH_REASON_RESCAN_BUTTON_CLICK)
+ listener.receiveSafetyCenterData()
+ listener.receiveSafetyCenterData()
+
+ SafetyCenterFlags.refreshTimeout = TIMEOUT_LONG
+ SafetySourceReceiver.safetySourceData[
+ SafetySourceDataKey(REFRESH_FETCH_FRESH_DATA, SINGLE_SOURCE_ID)] =
+ safetySourceCtsData.information
+ safetyCenterManager.refreshSafetySourcesWithReceiverPermissionAndWait(
+ REFRESH_REASON_RESCAN_BUTTON_CLICK)
+
+ val safetyCenterDataWhenTryingAgain = listener.receiveSafetyCenterData()
+ assertThat(safetyCenterDataWhenTryingAgain).isEqualTo(safetyCenterDataFromConfigScanning)
+ val safetyCenterDataWhenFinishingRefresh = listener.receiveSafetyCenterData()
+ assertThat(safetyCenterDataWhenFinishingRefresh).isEqualTo(safetyCenterDataOk)
+ }
+
+ @Test
fun refreshSafetySources_withRefreshReasonRescanButtonClick_notifiesUiDuringRescan() {
safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
SafetySourceReceiver.safetySourceData[
@@ -1427,6 +1556,38 @@ class SafetyCenterManagerTest {
}
@Test
+ fun getSafetyCenterData_withAccountIssue_returnsOverallStatusAccountRecommendationOneAlert() {
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ safetyCenterCtsHelper.setData(
+ SINGLE_SOURCE_ID, safetySourceCtsData.recommendationWithAccountIssue)
+
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+
+ assertThat(apiSafetyCenterData).isEqualTo(safetyCenterDataAccountRecommendationOneAlert)
+ }
+
+ @Test
+ fun getSafetyCenterData_withAccountIssue_returnsOverallStatusAccountCriticalOneAlert() {
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ safetyCenterCtsHelper.setData(
+ SINGLE_SOURCE_ID, safetySourceCtsData.criticalWithResolvingAccountIssue)
+
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+
+ assertThat(apiSafetyCenterData).isEqualTo(safetyCenterDataAccountCriticalOneAlert)
+ }
+
+ @Test
+ fun getSafetyCenterData_withAccountAndOtherIssue_returnsOverallStatusCriticalTwoAlerts() {
+ safetyCenterCtsHelper.setConfig(SINGLE_SOURCE_CONFIG)
+ safetyCenterCtsHelper.setData(SINGLE_SOURCE_ID, safetySourceCtsData.criticalWithTwoIssues)
+
+ val apiSafetyCenterData = safetyCenterManager.getSafetyCenterDataWithPermission()
+
+ assertThat(apiSafetyCenterData).isEqualTo(safetyCenterDataCriticalTwoAlerts)
+ }
+
+ @Test
fun getSafetyCenterData_withComplexConfigWithSomeDataProvided_returnsDataProvided() {
safetyCenterCtsHelper.setConfig(COMPLEX_CONFIG)
safetyCenterCtsHelper.setData(
@@ -2194,6 +2355,9 @@ class SafetyCenterManagerTest {
.setPendingIntent(safetySourceCtsData.redirectPendingIntent)
.setSeverityUnspecifiedIconType(SEVERITY_UNSPECIFIED_ICON_TYPE_NO_RECOMMENDATION)
+ private fun safetyCenterEntryError(sourceId: String) =
+ safetyCenterEntryDefaultBuilder(sourceId).setSummary("Couldn’t check status").build()
+
private fun safetyCenterEntryUnspecified(
sourceId: String,
pendingIntent: PendingIntent? = safetySourceCtsData.redirectPendingIntent
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterUnsupportedTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterUnsupportedTest.kt
index 62792d8dd..cdbeaaa2f 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterUnsupportedTest.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/SafetyCenterUnsupportedTest.kt
@@ -86,7 +86,7 @@ class SafetyCenterUnsupportedTest {
if (!shouldRunTests) {
return
}
- safetyCenterCtsHelper.setEnabled(true)
+ safetyCenterCtsHelper.setup()
}
@After
@@ -155,14 +155,18 @@ class SafetyCenterUnsupportedTest {
}
@Test
- fun reportSafetySourceError_doesntCheckRequest() {
+ fun reportSafetySourceError_doesntCallListener() {
safetyCenterManager.setSafetyCenterConfigForTestsWithPermission(SINGLE_SOURCE_CONFIG)
val listener = SafetyCenterCtsListener()
safetyCenterManager.addOnSafetyCenterDataChangedListenerWithPermission(
directExecutor(), listener)
safetyCenterManager.reportSafetySourceErrorWithPermission(
- "unknown_id", SafetySourceErrorDetails(EVENT_SOURCE_STATE_CHANGED))
+ SINGLE_SOURCE_ID, SafetySourceErrorDetails(EVENT_SOURCE_STATE_CHANGED))
+
+ assertFailsWith(TimeoutCancellationException::class) {
+ listener.receiveSafetyCenterData(TIMEOUT_SHORT)
+ }
}
@Test
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/config/XmlConfigTest.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/config/XmlConfigTest.kt
index ce7f6f6c5..7f46f21d6 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/config/XmlConfigTest.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/config/XmlConfigTest.kt
@@ -57,7 +57,7 @@ class XmlConfigTest {
if (!shouldRunTests) {
return
}
- safetyCenterCtsHelper.setEnabled(true)
+ safetyCenterCtsHelper.setup()
}
@After
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterCtsHelper.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterCtsHelper.kt
index 7adf054bd..a8e6af6c8 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterCtsHelper.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterCtsHelper.kt
@@ -22,6 +22,7 @@ import android.safetycenter.SafetyEvent
import android.safetycenter.SafetySourceData
import android.safetycenter.config.SafetyCenterConfig
import android.safetycenter.config.SafetySource.SAFETY_SOURCE_TYPE_STATIC
+import android.safetycenter.cts.testing.Coroutines.TIMEOUT_LONG
import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.addOnSafetyCenterDataChangedListenerWithPermission
import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.clearAllSafetySourceDataForTestsWithPermission
import android.safetycenter.cts.testing.SafetyCenterApisWithShellPermissions.clearSafetyCenterConfigForTestsWithPermission
@@ -43,6 +44,18 @@ class SafetyCenterCtsHelper(private val context: Context) {
private var currentConfigContainsCtsSource = false
+ /**
+ * Sets up the state of Safety Center by enabling it on the device and setting default flag
+ * values. To be called before each test.
+ */
+ fun setup() {
+ SafetyCenterFlags.showErrorEntriesOnTimeout = false
+ SafetyCenterFlags.resolveActionTimeout = TIMEOUT_LONG
+ SafetyCenterFlags.refreshTimeout = TIMEOUT_LONG
+ SafetyCenterFlags.untrackedSources = emptySet()
+ setEnabled(true)
+ }
+
/** Resets the state of Safety Center. To be called after each test. */
fun reset() {
setEnabled(true)
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterFlags.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterFlags.kt
index 362053c73..22b31e151 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterFlags.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetyCenterFlags.kt
@@ -33,6 +33,12 @@ object SafetyCenterFlags {
private const val PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
/**
+ * Flag that determines whether we should show error entries for sources that timeout when
+ * refreshing them.
+ */
+ private const val PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT = "show_error_entries_on_timeout"
+
+ /**
* Flag that determines 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 finished.
*/
@@ -77,6 +83,16 @@ object SafetyCenterFlags {
writeFlag(PROPERTY_SAFETY_CENTER_ENABLED, value.toString())
}
+ /** A property that allows getting and modifying [PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT]. */
+ var showErrorEntriesOnTimeout: Boolean
+ get() =
+ readFlag(PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT, defaultValue = false) {
+ it.toBoolean()
+ }
+ set(value) {
+ writeFlag(PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT, value.toString())
+ }
+
/**
* A property that allows getting and setting the
* [PROPERTY_SAFETY_CENTER_REFRESH_SOURCE_TIMEOUT] device config flag.
@@ -151,6 +167,7 @@ object SafetyCenterFlags {
DeviceConfig.getProperties(
NAMESPACE_PRIVACY,
PROPERTY_SAFETY_CENTER_ENABLED,
+ PROPERTY_SHOW_ERROR_ENTRIES_ON_TIMEOUT,
PROPERTY_SAFETY_CENTER_REFRESH_SOURCE_TIMEOUT,
PROPERTY_SAFETY_CENTER_RESOLVE_ACTION_TIMEOUT,
PROPERTY_UNTRACKED_SOURCES)
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceCtsData.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceCtsData.kt
index 8d6907b9c..9646378dd 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceCtsData.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceCtsData.kt
@@ -161,6 +161,23 @@ class SafetySourceCtsData(private val context: Context) {
.build())
.build()
+ /**
+ * A [SafetySourceIssue] with a [SEVERITY_LEVEL_RECOMMENDATION] and a redirecting [Action],
+ * related to the account.
+ */
+ val accountRecommendationIssue =
+ SafetySourceIssue.Builder(
+ RECOMMENDATION_ISSUE_ID,
+ "Recommendation issue title",
+ "Recommendation issue summary",
+ SEVERITY_LEVEL_RECOMMENDATION,
+ ISSUE_TYPE_ID)
+ .addAction(
+ Action.Builder(RECOMMENDATION_ISSUE_ACTION_ID, "See issue", redirectPendingIntent)
+ .build())
+ .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_ACCOUNT)
+ .build()
+
private val dismissIssuePendingIntent =
broadcastPendingIntent(
Intent(ACTION_HANDLE_DISMISSED_ISSUE).putExtra(EXTRA_SOURCE_ID, SINGLE_SOURCE_ID))
@@ -198,6 +215,22 @@ class SafetySourceCtsData(private val context: Context) {
.build()
/**
+ * A [SafetySourceData] with a [SEVERITY_LEVEL_RECOMMENDATION] redirecting [SafetySourceIssue]
+ * and [SafetySourceStatus], only containing an account issue.
+ */
+ val recommendationWithAccountIssue =
+ SafetySourceData.Builder()
+ .setStatus(
+ SafetySourceStatus.Builder(
+ "Recommendation title",
+ "Recommendation summary",
+ SEVERITY_LEVEL_RECOMMENDATION)
+ .setPendingIntent(redirectPendingIntent)
+ .build())
+ .addIssue(accountRecommendationIssue)
+ .build()
+
+ /**
* A [SafetySourceData] with a [SEVERITY_LEVEL_RECOMMENDATION] [SafetySourceIssue] that has a
* dismiss [PendingIntent], and [SafetySourceStatus].
*/
@@ -253,6 +286,25 @@ class SafetySourceCtsData(private val context: Context) {
.build()
/**
+ * Account related [SafetySourceIssue] with a [SEVERITY_LEVEL_CRITICAL_WARNING] and a resolving
+ * [Action].
+ */
+ val criticalResolvingAccountIssue =
+ SafetySourceIssue.Builder(
+ CRITICAL_ISSUE_ID,
+ "Critical issue title",
+ "Critical issue summary",
+ SEVERITY_LEVEL_CRITICAL_WARNING,
+ ISSUE_TYPE_ID)
+ .addAction(
+ Action.Builder(
+ CRITICAL_ISSUE_ACTION_ID, "Solve issue", criticalIssueActionPendingIntent)
+ .setWillResolve(true)
+ .build())
+ .setIssueCategory(SafetySourceIssue.ISSUE_CATEGORY_ACCOUNT)
+ .build()
+
+ /**
* A [SafetySourceData] with a [SEVERITY_LEVEL_CRITICAL_WARNING] resolving [SafetySourceIssue]
* and [SafetySourceStatus].
*/
@@ -281,6 +333,22 @@ class SafetySourceCtsData(private val context: Context) {
.build()
/**
+ * A [SafetySourceData] with a [SEVERITY_LEVEL_CRITICAL_WARNING] and a
+ * [SEVERITY_LEVEL_RECOMMENDATION] [SafetySourceIssue]s and [SEVERITY_LEVEL_CRITICAL_WARNING]
+ * [SafetySourceStatus]. One issue is account related, other isn't.
+ */
+ val criticalWithTwoIssues =
+ SafetySourceData.Builder()
+ .setStatus(
+ SafetySourceStatus.Builder(
+ "Critical title", "Critical summary", SEVERITY_LEVEL_CRITICAL_WARNING)
+ .setPendingIntent(redirectPendingIntent)
+ .build())
+ .addIssue(criticalResolvingAccountIssue)
+ .addIssue(recommendationIssue)
+ .build()
+
+ /**
* Another [SafetySourceData] with a [SEVERITY_LEVEL_CRITICAL_WARNING] redirecting
* [SafetySourceIssue] and [SafetySourceStatus].
*/
@@ -294,6 +362,20 @@ class SafetySourceCtsData(private val context: Context) {
.addIssue(criticalRedirectingIssue)
.build()
+ /**
+ * Another [SafetySourceData] with a [SEVERITY_LEVEL_CRITICAL_WARNING] resolving account related
+ * [SafetySourceIssue] and [SafetySourceStatus].
+ */
+ val criticalWithResolvingAccountIssue =
+ SafetySourceData.Builder()
+ .setStatus(
+ SafetySourceStatus.Builder(
+ "Critical title", "Critical summary", SEVERITY_LEVEL_CRITICAL_WARNING)
+ .setPendingIntent(redirectPendingIntent)
+ .build())
+ .addIssue(criticalResolvingAccountIssue)
+ .build()
+
private fun broadcastPendingIntent(intent: Intent): PendingIntent =
PendingIntent.getBroadcast(
context,
diff --git a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceReceiver.kt b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceReceiver.kt
index 434ecaa1f..11f692489 100644
--- a/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceReceiver.kt
+++ b/tests/cts/safetycenter/src/android/safetycenter/cts/testing/SafetySourceReceiver.kt
@@ -20,6 +20,7 @@ import android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.os.UserManager
import android.safetycenter.SafetyCenterManager
import android.safetycenter.SafetyCenterManager.ACTION_REFRESH_SAFETY_SOURCES
import android.safetycenter.SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED
@@ -51,6 +52,12 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
/** Broadcast receiver used for testing broadcasts sent to safety sources. */
class SafetySourceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
+ val userManager = context.getSystemService(UserManager::class.java)!!
+ if (!userManager.isSystemUser) {
+ // Ignore multi-users calls to this receiver for now, as we're not testing multi-users
+ // broadcasts. When we do, we'll ensure that they don't leak between the tests.
+ return
+ }
if (intent == null) {
throw IllegalArgumentException("Received null intent")
}