diff options
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") } |