diff options
417 files changed, 9633 insertions, 5620 deletions
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb index 0ccd9511d945..6b3278fc6925 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "7552332" + build_id: "8572644" target: "CtsShim" source_file: "aosp_arm64/CtsShimPriv.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "sc-dev" + git_branch: "tm-dev" transform: TRANSFORM_NONE transform_options { } diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb index 7e85c8f6697d..34c9c4d4fe08 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "7552332" + build_id: "8572644" target: "CtsShim" source_file: "aosp_arm64/CtsShim.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "sc-dev" + git_branch: "tm-dev" transform: TRANSFORM_NONE transform_options { } diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb index 20c27858e9ab..6897a027705f 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "7552332" + build_id: "8572644" target: "CtsShim" source_file: "aosp_x86_64/CtsShimPriv.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "sc-dev" + git_branch: "tm-dev" transform: TRANSFORM_NONE transform_options { } diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb index 13e3ae5b121b..6dfa7810f22d 100644 --- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb +++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "7552332" + build_id: "8572644" target: "CtsShim" source_file: "aosp_x86_64/CtsShim.apk" } @@ -8,7 +8,7 @@ drops { version: "" version_group: "" git_project: "platform/frameworks/base" - git_branch: "sc-dev" + git_branch: "tm-dev" transform: TRANSFORM_NONE transform_options { } diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java index 1b9cf2648a3f..7393bcde13b6 100644 --- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java +++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java @@ -27,7 +27,7 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledAfter; +import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; @@ -282,15 +282,14 @@ public class AlarmManager { public static final long ENABLE_USE_EXACT_ALARM = 218533173L; /** - * For apps targeting {@link Build.VERSION_CODES#TIRAMISU} or above, the permission - * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the user explicitly - * allows it from Settings. + * The permission {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the + * user explicitly allows it from Settings. * - * TODO (b/226439802): change to EnabledSince(T) after SDK finalization. + * TODO (b/226439802): Either enable it in the next SDK or replace it with a better alternative. * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2) + @Disabled public static final long SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = 226439802L; @UnsupportedAppUsage diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 2ea8592e883e..4aa9e84c10a9 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -30,6 +30,8 @@ import static android.app.AlarmManager.INTERVAL_HOUR; import static android.app.AlarmManager.RTC; import static android.app.AlarmManager.RTC_WAKEUP; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; +import static android.os.PowerExemptionManager.REASON_ALARM_MANAGER_ALARM_CLOCK; +import static android.os.PowerExemptionManager.REASON_DENIED; import static android.os.PowerExemptionManager.REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED; import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.os.PowerWhitelistManager.REASON_ALARM_MANAGER_WHILE_IDLE; @@ -329,6 +331,7 @@ public class AlarmManagerService extends SystemService { }); BroadcastOptions mOptsWithFgs = BroadcastOptions.makeBasic(); + BroadcastOptions mOptsWithFgsForAlarmClock = BroadcastOptions.makeBasic(); BroadcastOptions mOptsWithoutFgs = BroadcastOptions.makeBasic(); BroadcastOptions mOptsTimeBroadcast = BroadcastOptions.makeBasic(); ActivityOptions mActivityOptsRestrictBal = ActivityOptions.makeBasic(); @@ -565,9 +568,6 @@ public class AlarmManagerService extends SystemService { @VisibleForTesting static final String KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED = "kill_on_schedule_exact_alarm_revoked"; - @VisibleForTesting - static final String KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = - "schedule_exact_alarm_denied_by_default"; private static final long DEFAULT_MIN_FUTURITY = 5 * 1000; private static final long DEFAULT_MIN_INTERVAL = 60 * 1000; @@ -612,8 +612,6 @@ public class AlarmManagerService extends SystemService { private static final boolean DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED = true; - private static final boolean DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true; - // Minimum futurity of a new alarm public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY; @@ -701,14 +699,6 @@ public class AlarmManagerService extends SystemService { public boolean KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED = DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED; - /** - * When this is {@code true}, apps with the change - * {@link AlarmManager#SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT} enabled will not get - * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} unless the user grants it to them. - */ - public volatile boolean SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = - DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; - public boolean USE_TARE_POLICY = Settings.Global.DEFAULT_ENABLE_TARE == 1; private long mLastAllowWhileIdleWhitelistDuration = -1; @@ -743,9 +733,12 @@ public class AlarmManagerService extends SystemService { mOptsWithFgs.setTemporaryAppAllowlist(ALLOW_WHILE_IDLE_WHITELIST_DURATION, TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_ALARM_MANAGER_WHILE_IDLE, ""); + mOptsWithFgsForAlarmClock.setTemporaryAppAllowlist( + ALLOW_WHILE_IDLE_WHITELIST_DURATION, + TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED, + REASON_ALARM_MANAGER_ALARM_CLOCK, ""); mOptsWithoutFgs.setTemporaryAppAllowlist(ALLOW_WHILE_IDLE_WHITELIST_DURATION, - TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, - REASON_ALARM_MANAGER_WHILE_IDLE, ""); + TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, REASON_DENIED, ""); } } @@ -892,15 +885,6 @@ public class AlarmManagerService extends SystemService { KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED, DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED); break; - case KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT: - final boolean oldValue = SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT; - - SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = properties.getBoolean( - KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, - DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT); - handleScheduleExactAlarmDeniedByDefaultChange(oldValue, - SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT); - break; default: if (name.startsWith(KEY_PREFIX_STANDBY_QUOTA) && !standbyQuotaUpdated) { // The quotas need to be updated in order, so we can't just rely @@ -971,15 +955,6 @@ public class AlarmManagerService extends SystemService { } } - private void handleScheduleExactAlarmDeniedByDefaultChange(boolean oldValue, - boolean newValue) { - if (oldValue == newValue) { - return; - } - mHandler.obtainMessage(AlarmHandler.CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE, - newValue).sendToTarget(); - } - private void migrateAlarmsToNewStoreLocked() { final AlarmStore newStore = LAZY_BATCHING ? new LazyAlarmStore() : new BatchingAlarmStore(); @@ -1156,9 +1131,6 @@ public class AlarmManagerService extends SystemService { pw.print(KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED, KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED); pw.println(); - pw.print(KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, - SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT); - pw.println(); pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY); pw.println(); @@ -1744,6 +1716,7 @@ public class AlarmManagerService extends SystemService { public void onStart() { mInjector.init(); mOptsWithFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); + mOptsWithFgsForAlarmClock.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsWithoutFgs.setPendingIntentBackgroundActivityLaunchAllowed(false); mOptsTimeBroadcast.setPendingIntentBackgroundActivityLaunchAllowed(false); mActivityOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false); @@ -2741,7 +2714,12 @@ public class AlarmManagerService extends SystemService { if (isExactAlarmChangeEnabled(callingPackage, callingUserId)) { needsPermission = exact; lowerQuota = !exact; - idleOptions = exact ? mOptsWithFgs.toBundle() : mOptsWithoutFgs.toBundle(); + if (exact) { + idleOptions = (alarmClock != null) ? mOptsWithFgsForAlarmClock.toBundle() + : mOptsWithFgs.toBundle(); + } else { + idleOptions = mOptsWithoutFgs.toBundle(); + } } else { changeDisabled = true; needsPermission = false; @@ -2928,10 +2906,8 @@ public class AlarmManagerService extends SystemService { } private boolean isScheduleExactAlarmDeniedByDefault(String packageName, int userId) { - return mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT - && CompatChanges.isChangeEnabled( - AlarmManager.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, packageName, - UserHandle.of(userId)); + return CompatChanges.isChangeEnabled(AlarmManager.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, + packageName, UserHandle.of(userId)); } @NeverCompile // Avoid size overhead of debugging code. @@ -4707,7 +4683,6 @@ public class AlarmManagerService extends SystemService { public static final int REFRESH_EXACT_ALARM_CANDIDATES = 11; public static final int TARE_AFFORDABILITY_CHANGED = 12; public static final int CHECK_EXACT_ALARM_PERMISSION_ON_UPDATE = 13; - public static final int CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE = 14; AlarmHandler() { super(Looper.myLooper()); @@ -4827,32 +4802,6 @@ public class AlarmManagerService extends SystemService { removeExactAlarmsOnPermissionRevoked(uid, packageName, /*killUid = */false); } break; - case CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE: - final boolean defaultDenied = (Boolean) msg.obj; - - final int[] startedUserIds = mActivityManagerInternal.getStartedUserIds(); - for (int appId : mExactAlarmCandidates) { - for (int userId : startedUserIds) { - uid = UserHandle.getUid(userId, appId); - - final AndroidPackage packageForUid = - mPackageManagerInternal.getPackage(uid); - if (packageForUid == null) { - continue; - } - final String pkg = packageForUid.getPackageName(); - if (defaultDenied) { - if (!hasScheduleExactAlarmInternal(pkg, uid) - && !hasUseExactAlarmInternal(pkg, uid)) { - removeExactAlarmsOnPermissionRevoked(uid, pkg, true); - } - } else if (hasScheduleExactAlarmInternal(pkg, uid)) { - sendScheduleExactAlarmPermissionStateChangedBroadcast(pkg, - UserHandle.getUserId(uid)); - } - } - } - break; default: // nope, just ignore it break; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 36e1c941cbc6..fe99c71d9a9a 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -320,7 +320,6 @@ package android.app { method public void setDemoted(boolean); method public void setFgServiceShown(boolean); method public void setImportanceLockedByCriticalDeviceFunction(boolean); - method public void setImportanceLockedByOEM(boolean); method public void setImportantConversation(boolean); method public void setOriginalImportance(int); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index abd60177f884..5d1d225f4d2d 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -4617,8 +4617,8 @@ public class ActivityManager { try { getService().broadcastIntentWithFeature( null, null, intent, null, null, Activity.RESULT_OK, null, null, - null /*requiredPermissions*/, null /*excludedPermissions*/, appOp, null, false, - true, userId); + null /*requiredPermissions*/, null /*excludedPermissions*/, + null /*excludedPackages*/, appOp, null, false, true, userId); } catch (RemoteException ex) { } } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 49a5c9f78230..f2ea060b1694 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -568,14 +568,15 @@ public abstract class ActivityManagerInternal { public abstract void unregisterProcessObserver(IProcessObserver processObserver); /** - * Checks if there is an unfinished instrumentation that targets the given uid. + * Gets the uid of the instrumentation source if there is an unfinished instrumentation that + * targets the given uid. * * @param uid The uid to be checked for * - * @return True, if there is an instrumentation whose target application uid matches the given - * uid, false otherwise + * @return the uid of the instrumentation source, if there is an instrumentation whose target + * application uid matches the given uid, and {@link android.os.Process#INVALID_UID} otherwise. */ - public abstract boolean isUidCurrentlyInstrumented(int uid); + public abstract int getInstrumentationSourceUid(int uid); /** Is this a device owner app? */ public abstract boolean isDeviceOwner(int uid); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 3b1943bf86f6..a216021fc66b 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1335,9 +1335,17 @@ public class AppOpsManager { public static final int OP_ACCESS_RESTRICTED_SETTINGS = AppProtoEnums.APP_OP_ACCESS_RESTRICTED_SETTINGS; + /** + * Receive microphone audio from an ambient sound detection event + * + * @hide + */ + public static final int OP_RECEIVE_AMBIENT_TRIGGER_AUDIO = + AppProtoEnums.APP_OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 120; + public static final int _NUM_OP = 121; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -1800,6 +1808,14 @@ public class AppOpsManager { public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS = "android:access_restricted_settings"; + /** + * Receive microphone audio from an ambient sound detection event + * + * @hide + */ + public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = + "android:receive_ambient_trigger_audio"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2021,6 +2037,7 @@ public class AppOpsManager { OP_ESTABLISH_VPN_SERVICE, // OP_ESTABLISH_VPN_SERVICE OP_ESTABLISH_VPN_MANAGER, // OP_ESTABLISH_VPN_MANAGER OP_ACCESS_RESTRICTED_SETTINGS, // OP_ACCESS_RESTRICTED_SETTINGS + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, // RECEIVE_SOUNDTRIGGER_AUDIO }; /** @@ -2147,6 +2164,7 @@ public class AppOpsManager { OPSTR_ESTABLISH_VPN_SERVICE, OPSTR_ESTABLISH_VPN_MANAGER, OPSTR_ACCESS_RESTRICTED_SETTINGS, + OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO, }; /** @@ -2274,6 +2292,7 @@ public class AppOpsManager { "ESTABLISH_VPN_SERVICE", "ESTABLISH_VPN_MANAGER", "ACCESS_RESTRICTED_SETTINGS", + "RECEIVE_SOUNDTRIGGER_AUDIO", }; /** @@ -2402,6 +2421,7 @@ public class AppOpsManager { null, // no permission for OP_ESTABLISH_VPN_SERVICE null, // no permission for OP_ESTABLISH_VPN_MANAGER null, // no permission for OP_ACCESS_RESTRICTED_SETTINGS, + null, // no permission for OP_RECEIVE_SOUNDTRIGGER_AUDIO }; /** @@ -2529,7 +2549,8 @@ public class AppOpsManager { null, // NEARBY_WIFI_DEVICES null, // ESTABLISH_VPN_SERVICE null, // ESTABLISH_VPN_MANAGER - null, // ACCESS_RESTRICTED_SETTINGS, + null, // ACCESS_RESTRICTED_SETTINGS + null, // RECEIVE_SOUNDTRIGGER_AUDIO }; /** @@ -2656,7 +2677,8 @@ public class AppOpsManager { null, // NEARBY_WIFI_DEVICES null, // ESTABLISH_VPN_SERVICE null, // ESTABLISH_VPN_MANAGER - null, // ACCESS_RESTRICTED_SETTINGS, + null, // ACCESS_RESTRICTED_SETTINGS + null, // RECEIVE_SOUNDTRIGGER_AUDIO }; /** @@ -2765,7 +2787,7 @@ public class AppOpsManager { AppOpsManager.MODE_ERRORED, // OP_NO_ISOLATED_STORAGE AppOpsManager.MODE_ALLOWED, // PHONE_CALL_MICROPHONE AppOpsManager.MODE_ALLOWED, // PHONE_CALL_CAMERA - AppOpsManager.MODE_ALLOWED, // OP_RECORD_AUDIO_HOTWORD + AppOpsManager.MODE_ALLOWED, // RECORD_AUDIO_HOTWORD AppOpsManager.MODE_DEFAULT, // MANAGE_ONGOING_CALLS AppOpsManager.MODE_DEFAULT, // MANAGE_CREDENTIALS AppOpsManager.MODE_DEFAULT, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER @@ -2783,6 +2805,7 @@ public class AppOpsManager { AppOpsManager.MODE_ALLOWED, // ESTABLISH_VPN_SERVICE AppOpsManager.MODE_ALLOWED, // ESTABLISH_VPN_MANAGER AppOpsManager.MODE_ALLOWED, // ACCESS_RESTRICTED_SETTINGS, + AppOpsManager.MODE_ALLOWED, // RECEIVE_SOUNDTRIGGER_AUDIO }; /** @@ -2913,6 +2936,7 @@ public class AppOpsManager { false, // OP_ESTABLISH_VPN_SERVICE false, // OP_ESTABLISH_VPN_MANAGER true, // ACCESS_RESTRICTED_SETTINGS + false, // RECEIVE_SOUNDTRIGGER_AUDIO }; /** @@ -3040,6 +3064,7 @@ public class AppOpsManager { false, // OP_ESTABLISH_VPN_SERVICE false, // OP_ESTABLISH_VPN_MANAGER true, // ACCESS_RESTRICTED_SETTINGS + false, // RECEIVE_SOUNDTRIGGER_AUDIO }; /** diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index ac46066997ff..6d982ced385c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1193,7 +1193,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/, - AppOpsManager.OP_NONE, null, false, false, getUserId()); + null, AppOpsManager.OP_NONE, null, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1210,7 +1210,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, false, false, + null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1226,7 +1226,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, false, false, + null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1243,8 +1243,8 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, options, false, false, - getUserId()); + null /*excludedPermissions=*/, null /*excludedPackages*/, + AppOpsManager.OP_NONE, options, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1259,7 +1259,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, false, false, + null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, false, false, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1268,7 +1268,7 @@ class ContextImpl extends Context { @Override public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPermissions, - String[] excludedPermissions) { + String[] excludedPermissions, String[] excludedPackages) { warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { @@ -1276,7 +1276,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, excludedPermissions, - AppOpsManager.OP_NONE, null, false, false, getUserId()); + excludedPackages, AppOpsManager.OP_NONE, null, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1303,7 +1303,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - excludedPermissions, AppOpsManager.OP_NONE, options, false, false, + excludedPermissions, null, AppOpsManager.OP_NONE, options, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1321,7 +1321,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, appOp, null, false, false, getUserId()); + null /*excludedPermissions=*/, null, appOp, null, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1338,7 +1338,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, true, false, + null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, true, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1402,7 +1402,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, rd, initialCode, initialData, initialExtras, receiverPermissions, - null /*excludedPermissions=*/, appOp, options, true, false, getUserId()); + null /*excludedPermissions=*/, null, appOp, options, true, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1416,7 +1416,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/, - AppOpsManager.OP_NONE, null, false, false, user.getIdentifier()); + null, AppOpsManager.OP_NONE, null, false, false, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1439,8 +1439,8 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, options, false, false, - user.getIdentifier()); + null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, options, false, + false, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1457,7 +1457,8 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, receiverPermissions, - null /*excludedPermissions=*/, appOp, null, false, false, user.getIdentifier()); + null /*excludedPermissions=*/, null, appOp, null, false, false, + user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1508,7 +1509,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, rd, initialCode, initialData, initialExtras, receiverPermissions, - null /*excludedPermissions=*/, appOp, options, true, false, + null /*excludedPermissions=*/, null, appOp, options, true, false, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1550,7 +1551,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/, - AppOpsManager.OP_NONE, null, false, true, getUserId()); + null, AppOpsManager.OP_NONE, null, false, true, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1589,7 +1590,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/, - AppOpsManager.OP_NONE, options, false, true, getUserId()); + null, AppOpsManager.OP_NONE, options, false, true, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1625,7 +1626,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, rd, initialCode, initialData, initialExtras, null, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, true, true, + null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, true, true, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -1658,7 +1659,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/, - AppOpsManager.OP_NONE, null, false, true, user.getIdentifier()); + null, AppOpsManager.OP_NONE, null, false, true, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1673,7 +1674,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/, - AppOpsManager.OP_NONE, options, false, true, user.getIdentifier()); + null, AppOpsManager.OP_NONE, options, false, true, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1708,7 +1709,7 @@ class ContextImpl extends Context { ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, rd, initialCode, initialData, initialExtras, null, - null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, true, true, + null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, true, true, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 4efe9dfe7185..8367441b1b95 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -141,7 +141,7 @@ interface IActivityManager { int broadcastIntentWithFeature(in IApplicationThread caller, in String callingFeatureId, in Intent intent, in String resolvedType, in IIntentReceiver resultTo, int resultCode, in String resultData, in Bundle map, in String[] requiredPermissions, in String[] excludePermissions, - int appOp, in Bundle options, boolean serialized, boolean sticky, int userId); + in String[] excludePackages, int appOp, in Bundle options, boolean serialized, boolean sticky, int userId); void unbroadcastIntent(in IApplicationThread caller, in Intent intent, int userId); @UnsupportedAppUsage oneway void finishReceiver(in IBinder who, int resultCode, in String resultData, in Bundle map, diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index df9f2a3cbb25..da6a551175e3 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -77,6 +77,7 @@ interface INotificationManager boolean areNotificationsEnabledForPackage(String pkg, int uid); boolean areNotificationsEnabled(String pkg); int getPackageImportance(String pkg); + boolean isImportanceLocked(String pkg, int uid); List<String> getAllowedAssistantAdjustments(String pkg); void allowAssistantAdjustment(String adjustmentType); diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 99d7c63cde42..8984c4292023 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -1058,10 +1058,11 @@ public class Instrumentation { } /** - * Sends the key events corresponding to the text to the app being - * instrumented. - * - * @param text The text to be sent. + * Sends the key events that result in the given text being typed into the currently focused + * window, and waits for it to be processed. + * + * @param text The text to be sent. + * @see #sendKeySync(KeyEvent) */ public void sendStringSync(String text) { if (text == null) { @@ -1084,11 +1085,12 @@ public class Instrumentation { } /** - * Send a key event to the currently focused window/view and wait for it to - * be processed. Finished at some point after the recipient has returned - * from its event processing, though it may <em>not</em> have completely - * finished reacting from the event -- for example, if it needs to update - * its display as a result, it may still be in the process of doing that. + * Sends a key event to the currently focused window, and waits for it to be processed. + * <p> + * This method blocks until the recipient has finished handling the event. Note that the + * recipient may <em>not</em> have completely finished reacting from the event when this method + * returns. For example, it may still be in the process of updating its display or UI contents + * upon reacting to the injected event. * * @param event The event to send to the current focus. */ @@ -1116,34 +1118,42 @@ public class Instrumentation { } /** - * Sends an up and down key event sync to the currently focused window. + * Sends up and down key events with the given key code to the currently focused window, and + * waits for it to be processed. * - * @param key The integer keycode for the event. + * @param keyCode The key code for the events to send. + * @see #sendKeySync(KeyEvent) */ - public void sendKeyDownUpSync(int key) { - sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key)); - sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key)); + public void sendKeyDownUpSync(int keyCode) { + sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); } /** - * Higher-level method for sending both the down and up key events for a - * particular character key code. Equivalent to creating both KeyEvent - * objects by hand and calling {@link #sendKeySync}. The event appears - * as if it came from keyboard 0, the built in one. - * + * Sends up and down key events with the given key code to the currently focused window, and + * waits for it to be processed. + * <p> + * Equivalent to {@link #sendKeyDownUpSync(int)}. + * * @param keyCode The key code of the character to send. + * @see #sendKeySync(KeyEvent) */ public void sendCharacterSync(int keyCode) { - sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + sendKeyDownUpSync(keyCode); } - + /** - * Dispatch a pointer event. Finished at some point after the recipient has - * returned from its event processing, though it may <em>not</em> have - * completely finished reacting from the event -- for example, if it needs - * to update its display as a result, it may still be in the process of - * doing that. + * Dispatches a pointer event into a window owned by the instrumented application, and waits for + * it to be processed. + * <p> + * If the motion event being injected is targeted at a window that is not owned by the + * instrumented application, the input injection will fail. See {@link #getUiAutomation()} for + * injecting events into all windows. + * <p> + * This method blocks until the recipient has finished handling the event. Note that the + * recipient may <em>not</em> have completely finished reacting from the event when this method + * returns. For example, it may still be in the process of updating its display or UI contents + * upon reacting to the injected event. * * @param event A motion event describing the pointer action. (As noted in * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use @@ -1155,10 +1165,10 @@ public class Instrumentation { event.setSource(InputDevice.SOURCE_TOUCHSCREEN); } - syncInputTransactionsAndInjectEvent(event); + syncInputTransactionsAndInjectEventIntoSelf(event); } - private void syncInputTransactionsAndInjectEvent(MotionEvent event) { + private void syncInputTransactionsAndInjectEventIntoSelf(MotionEvent event) { final boolean syncBefore = event.getAction() == MotionEvent.ACTION_DOWN || event.isFromSource(InputDevice.SOURCE_MOUSE); final boolean syncAfter = event.getAction() == MotionEvent.ACTION_UP; @@ -1169,8 +1179,9 @@ public class Instrumentation { .syncInputTransactions(true /*waitForAnimations*/); } + // Direct the injected event into windows owned by the instrumentation target. InputManager.getInstance().injectInputEvent( - event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); + event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH, Process.myUid()); if (syncAfter) { WindowManagerGlobal.getWindowManagerService() @@ -1182,19 +1193,21 @@ public class Instrumentation { } /** - * Dispatch a trackball event. Finished at some point after the recipient has - * returned from its event processing, though it may <em>not</em> have - * completely finished reacting from the event -- for example, if it needs - * to update its display as a result, it may still be in the process of - * doing that. - * + * Dispatches a trackball event into the currently focused window, and waits for it to be + * processed. + * <p> + * This method blocks until the recipient has finished handling the event. Note that the + * recipient may <em>not</em> have completely finished reacting from the event when this method + * returns. For example, it may still be in the process of updating its display or UI contents + * upon reacting to the injected event. + * * @param event A motion event describing the trackball action. (As noted in * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use * {@link SystemClock#uptimeMillis()} as the timebase. */ public void sendTrackballEventSync(MotionEvent event) { validateNotAppThread(); - if ((event.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) == 0) { + if (!event.isFromSource(InputDevice.SOURCE_CLASS_TRACKBALL)) { event.setSource(InputDevice.SOURCE_TRACKBALL); } InputManager.getInstance().injectInputEvent(event, diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index 6f0b03aeb6f3..c9cc1a179102 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -253,7 +253,6 @@ public final class NotificationChannel implements Parcelable { // If this is a blockable system notification channel. private boolean mBlockableSystem = false; private int mAllowBubbles = DEFAULT_ALLOW_BUBBLE; - private boolean mImportanceLockedByOEM; private boolean mImportanceLockedDefaultApp; private String mParentId = null; private String mConversationId = null; @@ -322,13 +321,13 @@ public final class NotificationChannel implements Parcelable { mLightColor = in.readInt(); mBlockableSystem = in.readBoolean(); mAllowBubbles = in.readInt(); - mImportanceLockedByOEM = in.readBoolean(); mOriginalImportance = in.readInt(); mParentId = in.readString(); mConversationId = in.readString(); mDemoted = in.readBoolean(); mImportantConvo = in.readBoolean(); mDeletedTime = in.readLong(); + mImportanceLockedDefaultApp = in.readBoolean(); } @Override @@ -382,13 +381,13 @@ public final class NotificationChannel implements Parcelable { dest.writeInt(mLightColor); dest.writeBoolean(mBlockableSystem); dest.writeInt(mAllowBubbles); - dest.writeBoolean(mImportanceLockedByOEM); dest.writeInt(mOriginalImportance); dest.writeString(mParentId); dest.writeString(mConversationId); dest.writeBoolean(mDemoted); dest.writeBoolean(mImportantConvo); dest.writeLong(mDeletedTime); + dest.writeBoolean(mImportanceLockedDefaultApp); } /** @@ -851,14 +850,6 @@ public final class NotificationChannel implements Parcelable { * @hide */ @TestApi - public void setImportanceLockedByOEM(boolean locked) { - mImportanceLockedByOEM = locked; - } - - /** - * @hide - */ - @TestApi public void setImportanceLockedByCriticalDeviceFunction(boolean locked) { mImportanceLockedDefaultApp = locked; } @@ -1107,8 +1098,8 @@ public final class NotificationChannel implements Parcelable { out.attributeBoolean(null, ATT_IMP_CONVERSATION, isImportantConversation()); } - // mImportanceLockedDefaultApp and mImportanceLockedByOEM have a different source of - // truth and so aren't written to this xml file + // mImportanceLockedDefaultApp has a different source of truth and so isn't written to + // this xml file out.endTag(null, TAG_CHANNEL); } @@ -1251,7 +1242,6 @@ public final class NotificationChannel implements Parcelable { && Arrays.equals(mVibration, that.mVibration) && Objects.equals(getGroup(), that.getGroup()) && Objects.equals(getAudioAttributes(), that.getAudioAttributes()) - && mImportanceLockedByOEM == that.mImportanceLockedByOEM && mImportanceLockedDefaultApp == that.mImportanceLockedDefaultApp && mOriginalImportance == that.mOriginalImportance && Objects.equals(getParentChannelId(), that.getParentChannelId()) @@ -1267,7 +1257,7 @@ public final class NotificationChannel implements Parcelable { getUserLockedFields(), isFgServiceShown(), mVibrationEnabled, mShowBadge, isDeleted(), getDeletedTimeMs(), getGroup(), getAudioAttributes(), isBlockable(), mAllowBubbles, - mImportanceLockedByOEM, mImportanceLockedDefaultApp, mOriginalImportance, + mImportanceLockedDefaultApp, mOriginalImportance, mParentId, mConversationId, mDemoted, mImportantConvo); result = 31 * result + Arrays.hashCode(mVibration); return result; @@ -1312,7 +1302,6 @@ public final class NotificationChannel implements Parcelable { + ", mAudioAttributes=" + mAudioAttributes + ", mBlockableSystem=" + mBlockableSystem + ", mAllowBubbles=" + mAllowBubbles - + ", mImportanceLockedByOEM=" + mImportanceLockedByOEM + ", mImportanceLockedDefaultApp=" + mImportanceLockedDefaultApp + ", mOriginalImp=" + mOriginalImportance + ", mParent=" + mParentId diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 0a2b42121545..8647b9a2967c 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -11210,7 +11210,9 @@ public class DevicePolicyManager { * for enterprise use. * * An example of a supported preferential network service is the Enterprise - * slice on 5G networks. + * slice on 5G networks. For devices on 4G networks, the profile owner needs to additionally + * configure enterprise APN to set up data call for the preferential network service. + * These APNs can be added using {@link #addOverrideApn}. * * By default, preferential network service is disabled on the work profile and * fully managed devices, on supported carriers and devices. @@ -11260,7 +11262,9 @@ public class DevicePolicyManager { * {@see PreferentialNetworkServiceConfig} * * An example of a supported preferential network service is the Enterprise - * slice on 5G networks. + * slice on 5G networks. For devices on 4G networks, the profile owner needs to additionally + * configure enterprise APN to set up data call for the preferential network service. + * These APNs can be added using {@link #addOverrideApn}. * * By default, preferential network service is disabled on the work profile and fully managed * devices, on supported carriers and devices. Admins can explicitly enable it with this API. @@ -13782,18 +13786,13 @@ public class DevicePolicyManager { } /** - * Called by device owner or profile owner to add an override APN. + * Called by device owner or managed profile owner to add an override APN. * * <p>This method may returns {@code -1} if {@code apnSetting} conflicts with an existing * override APN. Update the existing conflicted APN with * {@link #updateOverrideApn(ComponentName, int, ApnSetting)} instead of adding a new entry. * <p>Two override APNs are considered to conflict when all the following APIs return * the same values on both override APNs: - * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: - * Only device owners can add APNs. - * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: - * Device and profile owners can add enterprise APNs - * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can add other type of APNs. * <ul> * <li>{@link ApnSetting#getOperatorNumeric()}</li> * <li>{@link ApnSetting#getApnName()}</li> @@ -13808,6 +13807,15 @@ public class DevicePolicyManager { * <li>{@link ApnSetting#getRoamingProtocol()}</li> * </ul> * + * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: + * Only device owners can add APNs. + * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: + * Both device owners and managed profile owners can add enterprise APNs + * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can add other type of APNs. + * Enterprise APNs are specific to the managed profile and do not override any user-configured + * VPNs. They are prerequisites for enabling preferential network service on the managed + * profile on 4G networks ({@link #setPreferentialNetworkServiceConfigs}). + * * @param admin which {@link DeviceAdminReceiver} this request is associated with * @param apnSetting the override APN to insert * @return The {@code id} of inserted override APN. Or {@code -1} when failed to insert into @@ -13830,7 +13838,7 @@ public class DevicePolicyManager { } /** - * Called by device owner or profile owner to update an override APN. + * Called by device owner or managed profile owner to update an override APN. * * <p>This method may returns {@code false} if there is no override APN with the given * {@code apnId}. @@ -13840,7 +13848,7 @@ public class DevicePolicyManager { * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: * Only device owners can update APNs. * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: - * Device and profile owners can update enterprise APNs + * Both device owners and managed profile owners can update enterprise APNs * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can update other type of APNs. * * @param admin which {@link DeviceAdminReceiver} this request is associated with @@ -13867,14 +13875,14 @@ public class DevicePolicyManager { } /** - * Called by device owner or profile owner to remove an override APN. + * Called by device owner or managed profile owner to remove an override APN. * * <p>This method may returns {@code false} if there is no override APN with the given * {@code apnId}. * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: * Only device owners can remove APNs. * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}: - * Device and profile owners can remove enterprise APNs + * Both device owners and managed profile owners can remove enterprise APNs * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can remove other type of APNs. * * @param admin which {@link DeviceAdminReceiver} this request is associated with @@ -13899,7 +13907,8 @@ public class DevicePolicyManager { } /** - * Called by device owner to get all override APNs inserted by device owner. + * Called by device owner or managed profile owner to get all override APNs inserted by + * device owner or managed profile owner previously using {@link #addOverrideApn}. * * @param admin which {@link DeviceAdminReceiver} this request is associated with * @return A list of override APNs inserted by device owner. @@ -13924,6 +13933,9 @@ public class DevicePolicyManager { * <p> Override APNs are separated from other APNs on the device, and can only be inserted or * modified by the device owner. When enabled, only override APNs are in use, any other APNs * are ignored. + * <p>Note: Enterprise APNs added by managed profile owners do not need to be enabled by + * this API. They are part of the preferential network service config and is controlled by + * {@link #setPreferentialNetworkServiceConfigs}. * * @param admin which {@link DeviceAdminReceiver} this request is associated with * @param enabled {@code true} if override APNs should be enabled, {@code false} otherwise @@ -14569,12 +14581,13 @@ public class DevicePolicyManager { } /** - * Called by Device owner to disable user control over apps. User will not be able to clear - * app data or force-stop packages. + * Called by a device owner or a profile owner to disable user control over apps. User will not + * be able to clear app data or force-stop packages. When called by a device owner, applies to + * all users on the device. * * @param admin which {@link DeviceAdminReceiver} this request is associated with * @param packages The package names for the apps. - * @throws SecurityException if {@code admin} is not a device owner. + * @throws SecurityException if {@code admin} is not a device owner or a profile owner. */ public void setUserControlDisabledPackages(@NonNull ComponentName admin, @NonNull List<String> packages) { @@ -14589,12 +14602,14 @@ public class DevicePolicyManager { } /** - * Returns the list of packages over which user control is disabled by the device owner. + * Returns the list of packages over which user control is disabled by a device or profile + * owner. * * @param admin which {@link DeviceAdminReceiver} this request is associated with - * @throws SecurityException if {@code admin} is not a device owner. + * @throws SecurityException if {@code admin} is not a device or profile owner. */ - public @NonNull List<String> getUserControlDisabledPackages(@NonNull ComponentName admin) { + @NonNull + public List<String> getUserControlDisabledPackages(@NonNull ComponentName admin) { throwIfParentInstance("getUserControlDisabledPackages"); if (mService != null) { try { diff --git a/core/java/android/app/admin/DevicePolicyResources.java b/core/java/android/app/admin/DevicePolicyResources.java index c8033fafbc4e..11b840fd4ad4 100644 --- a/core/java/android/app/admin/DevicePolicyResources.java +++ b/core/java/android/app/admin/DevicePolicyResources.java @@ -184,6 +184,12 @@ public final class DevicePolicyResources { PREFIX + "WORK_PROFILE_IT_ADMIN_CANT_RESET_SCREEN_LOCK"; /** + * Text shown on the CTA link shown to user to set a separate lock for work apps + */ + public static final String WORK_PROFILE_IT_ADMIN_CANT_RESET_SCREEN_LOCK_ACTION = + PREFIX + "WORK_PROFILE_IT_ADMIN_CANT_RESET_SCREEN_LOCK_ACTION"; + + /** * Message shown in screen lock picker for setting up a work profile screen lock */ public static final String WORK_PROFILE_SCREEN_LOCK_SETUP_MESSAGE = @@ -1492,6 +1498,45 @@ public final class DevicePolicyResources { * Content description for the work profile lock screen. */ public static final String WORK_LOCK_ACCESSIBILITY = PREFIX + "WORK_LOCK_ACCESSIBILITY"; + + /** + * Notification text displayed when screenshots are blocked by an IT admin. + */ + public static final String SCREENSHOT_BLOCKED_BY_ADMIN = + PREFIX + "SCREENSHOT_BLOCKED_BY_ADMIN"; + + /** + * Message shown when user is almost at the limit of password attempts where the + * profile will be removed. Accepts number of failed attempts and remaining failed + * attempts as params. + */ + public static final String KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE = + PREFIX + "KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE"; + + /** + * Message shown in dialog when user has exceeded the maximum attempts and the profile + * will be removed. Accepts number of failed attempts as a param. + */ + public static final String KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE = + PREFIX + "KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE"; + + /** + * Monitoring dialog subtitle for the section describing VPN. + */ + public static final String QS_DIALOG_MONITORING_VPN_SUBTITLE = + PREFIX + "QS_DIALOG_MONITORING_VPN_SUBTITLE"; + + /** + * Monitoring dialog subtitle for the section describing network logging. + */ + public static final String QS_DIALOG_MONITORING_NETWORK_SUBTITLE = + PREFIX + "QS_DIALOG_MONITORING_NETWORK_SUBTITLE"; + + /** + * Monitoring dialog subtitle for the section describing certificate authorities. + */ + public static final String QS_DIALOG_MONITORING_CA_CERT_SUBTITLE = + PREFIX + "QS_DIALOG_MONITORING_CA_CERT_SUBTITLE"; } /** diff --git a/core/java/android/app/admin/DevicePolicyResourcesManager.java b/core/java/android/app/admin/DevicePolicyResourcesManager.java index e8eb792f7fd7..2cc189f87ced 100644 --- a/core/java/android/app/admin/DevicePolicyResourcesManager.java +++ b/core/java/android/app/admin/DevicePolicyResourcesManager.java @@ -26,6 +26,7 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.RemoteException; +import android.provider.DeviceConfig; import android.util.DisplayMetrics; import android.util.Log; @@ -40,6 +41,9 @@ import java.util.function.Supplier; public class DevicePolicyResourcesManager { private static String TAG = "DevicePolicyResourcesManager"; + private static String DISABLE_RESOURCES_UPDATABILITY_FLAG = "disable_resources_updatability"; + private static boolean DEFAULT_DISABLE_RESOURCES_UPDATABILITY = false; + private final Context mContext; private final IDevicePolicyManager mService; @@ -194,16 +198,20 @@ public class DevicePolicyResourcesManager { Objects.requireNonNull(drawableSource, "drawableSource can't be null"); Objects.requireNonNull(defaultDrawableLoader, "defaultDrawableLoader can't be null"); - if (drawableId.equals(DevicePolicyResources.UNDEFINED)) { + if (drawableId.equals(DevicePolicyResources.UNDEFINED) + || DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER, + DISABLE_RESOURCES_UPDATABILITY_FLAG, + DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) { return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader); } + if (mService != null) { try { ParcelableResource resource = mService.getDrawable( drawableId, drawableStyle, drawableSource); if (resource == null) { - return ParcelableResource.loadDefaultDrawable( - defaultDrawableLoader); + return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader); } return resource.getDrawable( mContext, @@ -287,16 +295,20 @@ public class DevicePolicyResourcesManager { Objects.requireNonNull(drawableSource, "drawableSource can't be null"); Objects.requireNonNull(defaultDrawableLoader, "defaultDrawableLoader can't be null"); - if (drawableId.equals(DevicePolicyResources.UNDEFINED)) { + if (drawableId.equals(DevicePolicyResources.UNDEFINED) + || DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER, + DISABLE_RESOURCES_UPDATABILITY_FLAG, + DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) { return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader); } + if (mService != null) { try { ParcelableResource resource = mService.getDrawable( drawableId, drawableStyle, drawableSource); if (resource == null) { - return ParcelableResource.loadDefaultDrawable( - defaultDrawableLoader); + return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader); } return resource.getDrawable(mContext, density, defaultDrawableLoader); } catch (RemoteException e) { @@ -330,9 +342,14 @@ public class DevicePolicyResourcesManager { Objects.requireNonNull(drawableSource, "drawableSource can't be null"); Objects.requireNonNull(defaultIcon, "defaultIcon can't be null"); - if (drawableId.equals(DevicePolicyResources.UNDEFINED)) { + if (drawableId.equals(DevicePolicyResources.UNDEFINED) + || DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER, + DISABLE_RESOURCES_UPDATABILITY_FLAG, + DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) { return defaultIcon; } + if (mService != null) { try { ParcelableResource resource = mService.getDrawable( @@ -463,7 +480,10 @@ public class DevicePolicyResourcesManager { Objects.requireNonNull(stringId, "stringId can't be null"); Objects.requireNonNull(defaultStringLoader, "defaultStringLoader can't be null"); - if (stringId.equals(DevicePolicyResources.UNDEFINED)) { + if (stringId.equals(DevicePolicyResources.UNDEFINED) || DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER, + DISABLE_RESOURCES_UPDATABILITY_FLAG, + DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) { return ParcelableResource.loadDefaultString(defaultStringLoader); } if (mService != null) { @@ -508,7 +528,10 @@ public class DevicePolicyResourcesManager { Objects.requireNonNull(stringId, "stringId can't be null"); Objects.requireNonNull(defaultStringLoader, "defaultStringLoader can't be null"); - if (stringId.equals(DevicePolicyResources.UNDEFINED)) { + if (stringId.equals(DevicePolicyResources.UNDEFINED) || DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER, + DISABLE_RESOURCES_UPDATABILITY_FLAG, + DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) { return ParcelableResource.loadDefaultString(defaultStringLoader); } if (mService != null) { diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java index 9bb048d7a75f..f6de72b43de6 100644 --- a/core/java/android/app/backup/BackupTransport.java +++ b/core/java/android/app/backup/BackupTransport.java @@ -665,184 +665,292 @@ public class BackupTransport { @Override public void name(AndroidFuture<String> resultFuture) throws RemoteException { - String result = BackupTransport.this.name(); - resultFuture.complete(result); + try { + String result = BackupTransport.this.name(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void configurationIntent(AndroidFuture<Intent> resultFuture) throws RemoteException { - Intent result = BackupTransport.this.configurationIntent(); - resultFuture.complete(result); + try { + Intent result = BackupTransport.this.configurationIntent(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void currentDestinationString(AndroidFuture<String> resultFuture) throws RemoteException { - String result = BackupTransport.this.currentDestinationString(); - resultFuture.complete(result); + try { + String result = BackupTransport.this.currentDestinationString(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void dataManagementIntent(AndroidFuture<Intent> resultFuture) throws RemoteException { - Intent result = BackupTransport.this.dataManagementIntent(); - resultFuture.complete(result); + try { + Intent result = BackupTransport.this.dataManagementIntent(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void dataManagementIntentLabel(AndroidFuture<CharSequence> resultFuture) throws RemoteException { - CharSequence result = BackupTransport.this.dataManagementIntentLabel(); - resultFuture.complete(result); + try { + CharSequence result = BackupTransport.this.dataManagementIntentLabel(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void transportDirName(AndroidFuture<String> resultFuture) throws RemoteException { - String result = BackupTransport.this.transportDirName(); - resultFuture.complete(result); + try { + String result = BackupTransport.this.transportDirName(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void requestBackupTime(AndroidFuture<Long> resultFuture) throws RemoteException { - long result = BackupTransport.this.requestBackupTime(); - resultFuture.complete(result); + try { + long result = BackupTransport.this.requestBackupTime(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void initializeDevice(ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.initializeDevice(); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.initializeDevice(); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.performBackup(packageInfo, inFd, flags); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.performBackup(packageInfo, inFd, flags); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void clearBackupData(PackageInfo packageInfo, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.clearBackupData(packageInfo); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.clearBackupData(packageInfo); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void finishBackup(ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.finishBackup(); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.finishBackup(); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void getAvailableRestoreSets(AndroidFuture<List<RestoreSet>> resultFuture) throws RemoteException { - RestoreSet[] result = BackupTransport.this.getAvailableRestoreSets(); - resultFuture.complete(Arrays.asList(result)); + try { + RestoreSet[] result = BackupTransport.this.getAvailableRestoreSets(); + resultFuture.complete(Arrays.asList(result)); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void getCurrentRestoreSet(AndroidFuture<Long> resultFuture) throws RemoteException { - long result = BackupTransport.this.getCurrentRestoreSet(); - resultFuture.complete(result); + try { + long result = BackupTransport.this.getCurrentRestoreSet(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void startRestore(long token, PackageInfo[] packages, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.startRestore(token, packages); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.startRestore(token, packages); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void nextRestorePackage(AndroidFuture<RestoreDescription> resultFuture) throws RemoteException { - RestoreDescription result = BackupTransport.this.nextRestorePackage(); - resultFuture.complete(result); + try { + RestoreDescription result = BackupTransport.this.nextRestorePackage(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void getRestoreData(ParcelFileDescriptor outFd, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.getRestoreData(outFd); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.getRestoreData(outFd); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void finishRestore(ITransportStatusCallback callback) throws RemoteException { - BackupTransport.this.finishRestore(); - callback.onOperationComplete(); + try { + BackupTransport.this.finishRestore(); + callback.onOperationComplete(); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void requestFullBackupTime(AndroidFuture<Long> resultFuture) throws RemoteException { - long result = BackupTransport.this.requestFullBackupTime(); - resultFuture.complete(result); + try { + long result = BackupTransport.this.requestFullBackupTime(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket, int flags, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.performFullBackup(targetPackage, socket, flags); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.performFullBackup(targetPackage, socket, flags); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void checkFullBackupSize(long size, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.checkFullBackupSize(size); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.checkFullBackupSize(size); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void sendBackupData(int numBytes, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.sendBackupData(numBytes); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.sendBackupData(numBytes); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void cancelFullBackup(ITransportStatusCallback callback) throws RemoteException { - BackupTransport.this.cancelFullBackup(); - callback.onOperationComplete(); + try { + BackupTransport.this.cancelFullBackup(); + callback.onOperationComplete(); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup, AndroidFuture<Boolean> resultFuture) throws RemoteException { - boolean result = BackupTransport.this.isAppEligibleForBackup(targetPackage, - isFullBackup); - resultFuture.complete(result); + try { + boolean result = BackupTransport.this.isAppEligibleForBackup(targetPackage, + isFullBackup); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void getBackupQuota(String packageName, boolean isFullBackup, AndroidFuture<Long> resultFuture) throws RemoteException { - long result = BackupTransport.this.getBackupQuota(packageName, isFullBackup); - resultFuture.complete(result); + try { + long result = BackupTransport.this.getBackupQuota(packageName, isFullBackup); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void getTransportFlags(AndroidFuture<Integer> resultFuture) throws RemoteException { - int result = BackupTransport.this.getTransportFlags(); - resultFuture.complete(result); + try { + int result = BackupTransport.this.getTransportFlags(); + resultFuture.complete(result); + } catch (RuntimeException e) { + resultFuture.cancel(/* mayInterruptIfRunning */ true); + } } @Override public void getNextFullRestoreDataChunk(ParcelFileDescriptor socket, ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.getNextFullRestoreDataChunk(socket); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.getNextFullRestoreDataChunk(socket); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } @Override public void abortFullRestore(ITransportStatusCallback callback) throws RemoteException { - int result = BackupTransport.this.abortFullRestore(); - callback.onOperationCompleteWithStatus(result); + try { + int result = BackupTransport.this.abortFullRestore(); + callback.onOperationCompleteWithStatus(result); + } catch (RuntimeException e) { + callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR); + } } } } diff --git a/core/java/android/app/smartspace/SmartspaceTarget.java b/core/java/android/app/smartspace/SmartspaceTarget.java index c5982fc27358..79d7b216628f 100644 --- a/core/java/android/app/smartspace/SmartspaceTarget.java +++ b/core/java/android/app/smartspace/SmartspaceTarget.java @@ -174,7 +174,7 @@ public final class SmartspaceTarget implements Parcelable { public static final int FEATURE_MEDIA_HEADS_UP = 36; public static final int FEATURE_STEP_COUNTING = 37; public static final int FEATURE_EARTHQUAKE_ALERT = 38; - public static final int FEATURE_STEP_DATE = 39; + public static final int FEATURE_STEP_DATE = 39; // This represents a DATE. "STEP" is a typo. public static final int FEATURE_BLAZE_BUILD_PROGRESS = 40; public static final int FEATURE_EARTHQUAKE_OCCURRED = 41; diff --git a/core/java/android/companion/virtual/OWNERS b/core/java/android/companion/virtual/OWNERS new file mode 100644 index 000000000000..29681045ac4a --- /dev/null +++ b/core/java/android/companion/virtual/OWNERS @@ -0,0 +1 @@ +include /services/companion/java/com/android/server/companion/virtual/OWNERS diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 907db7df68d5..836bff598ede 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2269,6 +2269,19 @@ public abstract class Context { */ public void sendBroadcastMultiplePermissions(@NonNull Intent intent, @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions) { + sendBroadcastMultiplePermissions(intent, receiverPermissions, excludedPermissions, null); + } + + + /** + * Like {@link #sendBroadcastMultiplePermissions(Intent, String[], String[])}, but also allows + * specification of a list of excluded packages. + * + * @hide + */ + public void sendBroadcastMultiplePermissions(@NonNull Intent intent, + @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions, + @Nullable String[] excludedPackages) { throw new RuntimeException("Not implemented. Must override in a subclass."); } diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 4ecd7761ac4f..e6549187e5c5 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -515,8 +515,10 @@ public class ContextWrapper extends Context { /** @hide */ @Override public void sendBroadcastMultiplePermissions(@NonNull Intent intent, - @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions) { - mBase.sendBroadcastMultiplePermissions(intent, receiverPermissions, excludedPermissions); + @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions, + @Nullable String[] excludedPackages) { + mBase.sendBroadcastMultiplePermissions(intent, receiverPermissions, excludedPermissions, + excludedPackages); } /** @hide */ diff --git a/core/java/android/content/pm/AppSearchShortcutInfo.java b/core/java/android/content/pm/AppSearchShortcutInfo.java index 1b84686bbfcf..fb41b890ce9c 100644 --- a/core/java/android/content/pm/AppSearchShortcutInfo.java +++ b/core/java/android/content/pm/AppSearchShortcutInfo.java @@ -413,7 +413,10 @@ public class AppSearchShortcutInfo extends GenericDocument { final int iconResId = (int) getPropertyLong(KEY_ICON_RES_ID); final String iconResName = getPropertyString(KEY_ICON_RES_NAME); final String iconUri = getPropertyString(KEY_ICON_URI); - final int disabledReason = Integer.parseInt(getPropertyString(KEY_DISABLED_REASON)); + final String disabledReasonString = getPropertyString(KEY_DISABLED_REASON); + final int disabledReason = !TextUtils.isEmpty(disabledReasonString) + ? Integer.parseInt(getPropertyString(KEY_DISABLED_REASON)) + : ShortcutInfo.DISABLED_REASON_NOT_DISABLED; final Map<String, Map<String, List<String>>> capabilityBindings = parseCapabilityBindings(getPropertyStringArray(KEY_CAPABILITY_BINDINGS)); return new ShortcutInfo( diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index e1ffd4a6761d..2da12e6c5c9d 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -57,11 +57,16 @@ interface IInputManager { // Temporarily changes the pointer speed. void tryPointerSpeed(int speed); - // Injects an input event into the system. To inject into windows owned by other - // applications, the caller must have the INJECT_EVENTS permission. + // Injects an input event into the system. The caller must have the INJECT_EVENTS permssion. + // This method exists only for compatibility purposes and may be removed in a future release. @UnsupportedAppUsage boolean injectInputEvent(in InputEvent ev, int mode); + // Injects an input event into the system. The caller must have the INJECT_EVENTS permission. + // The caller can target windows owned by a certain UID by providing a valid UID, or by + // providing {@link android.os.Process#INVALID_UID} to target all windows. + boolean injectInputEventToTarget(in InputEvent ev, int mode, int targetUid); + VerifiedInputEvent verifyInputEvent(in InputEvent ev); // Calibrate input device position diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index cc5b275bbf5a..d17a9523ab37 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -45,6 +45,7 @@ import android.os.IVibratorStateListener; import android.os.InputEventInjectionSync; import android.os.Looper; import android.os.Message; +import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -1107,14 +1108,18 @@ public final class InputManager { } } - /** - * Injects an input event into the event system on behalf of an application. + * Injects an input event into the event system, targeting windows owned by the provided uid. + * + * If a valid targetUid is provided, the system will only consider injecting the input event + * into windows owned by the provided uid. If the input event is targeted at a window that is + * not owned by the provided uid, input injection will fail and a RemoteException will be + * thrown. + * * The synchronization mode determines whether the method blocks while waiting for * input injection to proceed. * <p> - * Requires {@link android.Manifest.permission.INJECT_EVENTS} to inject into - * windows that are owned by other applications. + * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission. * </p><p> * Make sure you correctly set the event time and input source of the event * before calling this method. @@ -1125,12 +1130,14 @@ public final class InputManager { * {@link android.os.InputEventInjectionSync.NONE}, * {@link android.os.InputEventInjectionSync.WAIT_FOR_RESULT}, or * {@link android.os.InputEventInjectionSync.WAIT_FOR_FINISHED}. + * @param targetUid The uid to target, or {@link android.os.Process#INVALID_UID} to target all + * windows. * @return True if input event injection succeeded. * * @hide */ - @UnsupportedAppUsage - public boolean injectInputEvent(InputEvent event, int mode) { + @RequiresPermission(Manifest.permission.INJECT_EVENTS) + public boolean injectInputEvent(InputEvent event, int mode, int targetUid) { if (event == null) { throw new IllegalArgumentException("event must not be null"); } @@ -1141,13 +1148,39 @@ public final class InputManager { } try { - return mIm.injectInputEvent(event, mode); + return mIm.injectInputEventToTarget(event, mode, targetUid); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } /** + * Injects an input event into the event system on behalf of an application. + * The synchronization mode determines whether the method blocks while waiting for + * input injection to proceed. + * <p> + * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission. + * </p><p> + * Make sure you correctly set the event time and input source of the event + * before calling this method. + * </p> + * + * @param event The event to inject. + * @param mode The synchronization mode. One of: + * {@link android.os.InputEventInjectionSync.NONE}, + * {@link android.os.InputEventInjectionSync.WAIT_FOR_RESULT}, or + * {@link android.os.InputEventInjectionSync.WAIT_FOR_FINISHED}. + * @return True if input event injection succeeded. + * + * @hide + */ + @RequiresPermission(Manifest.permission.INJECT_EVENTS) + @UnsupportedAppUsage + public boolean injectInputEvent(InputEvent event, int mode) { + return injectInputEvent(event, mode, Process.INVALID_UID); + } + + /** * Verify the details of an {@link android.view.InputEvent} that came from the system. * If the event did not come from the system, or its details could not be verified, then this * will return {@code null}. Receiving {@code null} does not mean that the event did not diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 25296bc0a8b9..200fe22edaad 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -1625,7 +1625,6 @@ public class InputMethodService extends AbstractInputMethodService { // when IME developers are doing something unsupported. InputMethodPrivilegedOperationsRegistry.remove(mToken); } - unregisterCompatOnBackInvokedCallback(); mImeDispatcher = null; } @@ -2788,6 +2787,11 @@ public class InputMethodService extends AbstractInputMethodService { if (mInkWindow != null) { finishStylusHandwriting(); } + // Back callback is typically unregistered in {@link #hideWindow()}, but it's possible + // for {@link #doFinishInput()} to be called without {@link #hideWindow()} so we also + // unregister here. + // TODO(b/232341407): Add CTS to verify back behavior after screen on / off. + unregisterCompatOnBackInvokedCallback(); } void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) { diff --git a/core/java/android/net/IVpnManager.aidl b/core/java/android/net/IVpnManager.aidl index b4647cabe1bc..f30237853a3e 100644 --- a/core/java/android/net/IVpnManager.aidl +++ b/core/java/android/net/IVpnManager.aidl @@ -42,6 +42,8 @@ interface IVpnManager { String startVpnProfile(String packageName); void stopVpnProfile(String packageName); VpnProfileState getProvisionedVpnProfileState(String packageName); + boolean setAppExclusionList(int userId, String vpnPackage, in List<String> excludedApps); + List<String> getAppExclusionList(int userId, String vpnPackage); /** Always-on VPN APIs */ boolean isAlwaysOnVpnPackageSupported(int userId, String packageName); diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java index ae7d91f92cb7..f62d7c4a698d 100644 --- a/core/java/android/net/VpnManager.java +++ b/core/java/android/net/VpnManager.java @@ -187,14 +187,24 @@ public class VpnManager { /** * The network that was underlying the VPN when the event occurred, as a {@link Network}. * - * This extra will be null if there was no underlying network at the time of the event. + * <p>This extra will be null if there was no underlying network at the time of the event, or + * the underlying network has no bearing on the event, as in the case of: + * <ul> + * <li>CATEGORY_EVENT_DEACTIVATED_BY_USER + * <li>CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED + * </ul> */ public static final String EXTRA_UNDERLYING_NETWORK = "android.net.extra.UNDERLYING_NETWORK"; /** * The {@link NetworkCapabilities} of the underlying network when the event occurred. * - * This extra will be null if there was no underlying network at the time of the event. + * <p>This extra will be null if there was no underlying network at the time of the event, or + * the underlying network has no bearing on the event, as in the case of: + * <ul> + * <li>CATEGORY_EVENT_DEACTIVATED_BY_USER + * <li>CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED + * </ul> */ public static final String EXTRA_UNDERLYING_NETWORK_CAPABILITIES = "android.net.extra.UNDERLYING_NETWORK_CAPABILITIES"; @@ -202,7 +212,12 @@ public class VpnManager { /** * The {@link LinkProperties} of the underlying network when the event occurred. * - * This extra will be null if there was no underlying network at the time of the event. + * <p>This extra will be null if there was no underlying network at the time of the event, or + * the underlying network has no bearing on the event, as in the case of: + * <ul> + * <li>CATEGORY_EVENT_DEACTIVATED_BY_USER + * <li>CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED + * </ul> */ public static final String EXTRA_UNDERLYING_LINK_PROPERTIES = "android.net.extra.UNDERLYING_LINK_PROPERTIES"; @@ -580,6 +595,63 @@ public class VpnManager { } /** + * Sets the application exclusion list for the specified VPN profile. + * + * <p>If an app in the set of excluded apps is not installed for the given user, it will be + * skipped in the list of app exclusions. If apps are installed or removed, any active VPN will + * have its UID set updated automatically. If the caller is not {@code userId}, + * {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required. + * + * <p>This will ONLY affect VpnManager profiles. As such, the NETWORK_SETTINGS provider MUST NOT + * allow configuration of these options if the application has not provided a VPN profile. + * + * @param userId the identifier of the user to set app exclusion list + * @param vpnPackage The package name for an installed VPN app on the device + * @param excludedApps the app exclusion list + * @throws IllegalStateException exception if vpn for the @code userId} is not ready yet. + * + * @return whether setting the list is successful or not + * @hide + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public boolean setAppExclusionList(int userId, @NonNull String vpnPackage, + @NonNull List<String> excludedApps) { + try { + return mService.setAppExclusionList(userId, vpnPackage, excludedApps); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Gets the application exclusion list for the specified VPN profile. If the caller is not + * {@code userId}, {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission + * is required. + * + * @param userId the identifier of the user to set app exclusion list + * @param vpnPackage The package name for an installed VPN app on the device + * @return the list of packages for the specified VPN profile or null if no corresponding VPN + * profile configured. + * + * @hide + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + @Nullable + public List<String> getAppExclusionList(int userId, @NonNull String vpnPackage) { + try { + return mService.getAppExclusionList(userId, vpnPackage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * @return the list of packages that are allowed to access network when always-on VPN is in * lockdown mode but not connected. Returns {@code null} when VPN lockdown is not active. * diff --git a/core/java/android/net/VpnProfileState.java b/core/java/android/net/VpnProfileState.java index c69ea1a8c220..552a2c171f21 100644 --- a/core/java/android/net/VpnProfileState.java +++ b/core/java/android/net/VpnProfileState.java @@ -24,6 +24,8 @@ import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.StringJoiner; /** * Describe the state of VPN. @@ -150,4 +152,44 @@ public final class VpnProfileState implements Parcelable { mAlwaysOn = in.readBoolean(); mLockdown = in.readBoolean(); } + + private String convertStateToString(@State int state) { + switch (state) { + case STATE_CONNECTED: + return "CONNECTED"; + case STATE_CONNECTING: + return "CONNECTING"; + case STATE_DISCONNECTED: + return "DISCONNECTED"; + case STATE_FAILED: + return "FAILED"; + default: + return "UNKNOWN"; + } + } + + @Override + public String toString() { + final StringJoiner resultJoiner = new StringJoiner(", ", "{", "}"); + resultJoiner.add("State: " + convertStateToString(getState())); + resultJoiner.add("SessionId: " + getSessionId()); + resultJoiner.add("Always-on: " + isAlwaysOn()); + resultJoiner.add("Lockdown: " + isLockdownEnabled()); + return resultJoiner.toString(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof VpnProfileState)) return false; + final VpnProfileState that = (VpnProfileState) obj; + return (getState() == that.getState() + && Objects.equals(getSessionId(), that.getSessionId()) + && isAlwaysOn() == that.isAlwaysOn() + && isLockdownEnabled() == that.isLockdownEnabled()); + } + + @Override + public int hashCode() { + return Objects.hash(getState(), getSessionId(), isAlwaysOn(), isLockdownEnabled()); + } } diff --git a/core/java/android/permission/PermissionUsageHelper.java b/core/java/android/permission/PermissionUsageHelper.java index 4ed939c48bd7..f5f1c374b636 100644 --- a/core/java/android/permission/PermissionUsageHelper.java +++ b/core/java/android/permission/PermissionUsageHelper.java @@ -30,6 +30,7 @@ import static android.app.AppOpsManager.OPSTR_COARSE_LOCATION; import static android.app.AppOpsManager.OPSTR_FINE_LOCATION; import static android.app.AppOpsManager.OPSTR_PHONE_CALL_CAMERA; import static android.app.AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE; +import static android.app.AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO; import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED; @@ -137,6 +138,7 @@ public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedLis private static final List<String> MIC_OPS = List.of( OPSTR_PHONE_CALL_MICROPHONE, + OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO, OPSTR_RECORD_AUDIO ); @@ -147,6 +149,7 @@ public class PermissionUsageHelper implements AppOpsManager.OnOpActiveChangedLis private static @NonNull String getGroupForOp(String op) { switch (op) { + case OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO: case OPSTR_RECORD_AUDIO: return MICROPHONE; case OPSTR_CAMERA: diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java index 9a2f7baa7265..13a3ec8c0562 100644 --- a/core/java/android/provider/DeviceConfig.java +++ b/core/java/android/provider/DeviceConfig.java @@ -607,6 +607,14 @@ public final class DeviceConfig { public static final String NAMESPACE_VOICE_INTERACTION = "voice_interaction"; /** + * Namespace for DevicePolicyManager related features. + * + * @hide + */ + public static final String NAMESPACE_DEVICE_POLICY_MANAGER = + "device_policy_manager"; + + /** * List of namespaces which can be read without READ_DEVICE_CONFIG permission * * @hide @@ -614,7 +622,8 @@ public final class DeviceConfig { @NonNull private static final List<String> PUBLIC_NAMESPACES = Arrays.asList(NAMESPACE_TEXTCLASSIFIER, NAMESPACE_RUNTIME, NAMESPACE_STATSD_JAVA, - NAMESPACE_STATSD_JAVA_BOOT, NAMESPACE_SELECTION_TOOLBAR, NAMESPACE_AUTOFILL); + NAMESPACE_STATSD_JAVA_BOOT, NAMESPACE_SELECTION_TOOLBAR, NAMESPACE_AUTOFILL, + NAMESPACE_DEVICE_POLICY_MANAGER); /** * Privacy related properties definitions. * @@ -738,14 +747,6 @@ public final class DeviceConfig { */ public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native"; - /** - * Namespace for DevicePolicyManager related features. - * - * @hide - */ - public static final String NAMESPACE_DEVICE_POLICY_MANAGER = - "device_policy_manager"; - private static final Object sLock = new Object(); @GuardedBy("sLock") private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners = diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java index 01d5638461af..b2bf9bc2ddd4 100644 --- a/core/java/android/service/voice/AbstractHotwordDetector.java +++ b/core/java/android/service/voice/AbstractHotwordDetector.java @@ -189,5 +189,14 @@ abstract class AbstractHotwordDetector implements HotwordDetector { .setHotwordDetectedResult(hotwordDetectedResult) .build())); } + + /** Called when the detection fails due to an error. */ + @Override + public void onError() { + Slog.v(TAG, "BinderCallback#onError"); + mHandler.sendMessage(obtainMessage( + HotwordDetector.Callback::onError, + mCallback)); + } } } diff --git a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl index 80f20fe405b1..e8650894ac14 100644 --- a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl +++ b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl @@ -33,4 +33,9 @@ oneway interface IMicrophoneHotwordDetectionVoiceInteractionCallback { in HotwordDetectedResult hotwordDetectedResult, in AudioFormat audioFormat, in ParcelFileDescriptor audioStream); + + /** + * Called when the detection fails due to an error. + */ + void onError(); } diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java index 2d662eaf0a4f..f5a0c66f7b1b 100644 --- a/core/java/android/service/voice/SoftwareHotwordDetector.java +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -155,6 +155,15 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { .setHotwordDetectedResult(hotwordDetectedResult) .build())); } + + /** Called when the detection fails due to an error. */ + @Override + public void onError() { + Slog.v(TAG, "BinderCallback#onError"); + mHandler.sendMessage(obtainMessage( + HotwordDetector.Callback::onError, + mCallback)); + } } private static class InitializationStateListener diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 0ec95c687090..d598017dacaa 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -888,7 +888,6 @@ public abstract class WallpaperService extends Service { if (mShouldDimByDefault != mShouldDim && mWallpaperDimAmount == 0f) { mShouldDim = mShouldDimByDefault; updateSurfaceDimming(); - updateSurface(false, false, true); } } @@ -898,6 +897,10 @@ public abstract class WallpaperService extends Service { * @param dimAmount Float amount between [0.0, 1.0] to dim the wallpaper. */ private void updateWallpaperDimming(float dimAmount) { + if (dimAmount == mWallpaperDimAmount) { + return; + } + // Custom dim amount cannot be less than the default dim amount. mWallpaperDimAmount = Math.max(mDefaultDimAmount, dimAmount); // If dim amount is 0f (additional dimming is removed), then the wallpaper should dim @@ -1195,7 +1198,6 @@ public abstract class WallpaperService extends Service { .setParent(mSurfaceControl) .setCallsite("Wallpaper#relayout") .build(); - updateSurfaceDimming(); } // Propagate transform hint from WM, so we can use the right hint for the // first frame. @@ -1366,7 +1368,6 @@ public abstract class WallpaperService extends Service { mSession.finishDrawing(mWindow, null /* postDrawTransaction */, Integer.MAX_VALUE); processLocalColors(mPendingXOffset, mPendingXOffsetStep); - notifyColorsChanged(); } reposition(); reportEngineShown(shouldWaitForEngineShown()); diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index e5ec260907df..691452f51ee8 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -341,10 +341,12 @@ public class Surface implements Parcelable { */ @UnsupportedAppUsage public void destroy() { - if (mNativeObject != 0) { - nativeDestroy(mNativeObject); + synchronized (mLock) { + if (mNativeObject != 0) { + nativeDestroy(mNativeObject); + } + release(); } - release(); } /** diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index d04b07c13b41..38ca2481726b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -11941,7 +11941,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @NonNull public final List<Rect> getUnrestrictedPreferKeepClearRects() { final ListenerInfo info = mListenerInfo; - if (info != null && info.mKeepClearRects != null) { + if (info != null && info.mUnrestrictedKeepClearRects != null) { return new ArrayList(info.mUnrestrictedKeepClearRects); } @@ -21170,6 +21170,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } AccessibilityNodeIdManager.getInstance().unregisterViewWithId(getAccessibilityViewId()); + + if (mBackgroundRenderNode != null) { + mBackgroundRenderNode.forceEndAnimators(); + } + mRenderNode.forceEndAnimators(); } private void cleanupDraw() { diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java index d5763aa25884..8bdf233b81c8 100644 --- a/core/java/android/window/ImeOnBackInvokedDispatcher.java +++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java @@ -158,6 +158,12 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc /** Clears all registered callbacks on the instance. */ public void clear() { + // Unregister previously registered callbacks if there's any. + if (getReceivingDispatcher() != null) { + for (OnBackInvokedCallback callback : mImeCallbackMap.values()) { + getReceivingDispatcher().unregisterOnBackInvokedCallback(callback); + } + } mImeCallbackMap.clear(); } diff --git a/core/java/com/android/internal/app/AppLocaleStore.java b/core/java/com/android/internal/app/AppLocaleStore.java index 599e6d24600c..f3a322cac79a 100644 --- a/core/java/com/android/internal/app/AppLocaleStore.java +++ b/core/java/com/android/internal/app/AppLocaleStore.java @@ -20,12 +20,15 @@ import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStat import android.app.LocaleConfig; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.InstallSourceInfo; import android.content.pm.PackageManager; import android.os.LocaleList; import android.util.Log; -import java.util.ArrayList; +import java.util.HashSet; import java.util.Locale; +import java.util.stream.Collectors; class AppLocaleStore { private static final String TAG = AppLocaleStore.class.getSimpleName(); @@ -34,7 +37,8 @@ class AppLocaleStore { Context context, String packageName) { LocaleConfig localeConfig = null; AppLocaleResult.LocaleStatus localeStatus = LocaleStatus.UNKNOWN_FAILURE; - ArrayList<Locale> appSupportedLocales = new ArrayList<>(); + HashSet<Locale> appSupportedLocales = new HashSet<>(); + HashSet<Locale> assetLocale = getAssetLocales(context, packageName); try { localeConfig = new LocaleConfig(context.createPackageContext(packageName, 0)); @@ -45,32 +49,43 @@ class AppLocaleStore { if (localeConfig != null) { if (localeConfig.getStatus() == LocaleConfig.STATUS_SUCCESS) { LocaleList packageLocaleList = localeConfig.getSupportedLocales(); - if (packageLocaleList.size() > 0) { + boolean shouldFilterNotMatchingLocale = !hasInstallerInfo(context, packageName) && + isSystemApp(context, packageName); + + Log.d(TAG, "filterNonMatchingLocale. " + + ", shouldFilterNotMatchingLocale: " + shouldFilterNotMatchingLocale + + ", assetLocale size: " + assetLocale.size() + + ", packageLocaleList size: " + packageLocaleList.size()); + + for (int i = 0; i < packageLocaleList.size(); i++) { + appSupportedLocales.add(packageLocaleList.get(i)); + } + if (shouldFilterNotMatchingLocale) { + appSupportedLocales = filterNotMatchingLocale(appSupportedLocales, assetLocale); + } + + if (appSupportedLocales.size() > 0) { localeStatus = LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG; - for (int i = 0; i < packageLocaleList.size(); i++) { - appSupportedLocales.add(packageLocaleList.get(i)); - } } else { localeStatus = LocaleStatus.NO_SUPPORTED_LANGUAGE_IN_APP; } } else if (localeConfig.getStatus() == LocaleConfig.STATUS_NOT_SPECIFIED) { - String[] languages = getAssetLocales(context, packageName); - if (languages.length > 0) { + if (assetLocale.size() > 0) { localeStatus = LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; - for (String language : languages) { - appSupportedLocales.add(Locale.forLanguageTag(language)); - } + appSupportedLocales = assetLocale; } else { localeStatus = LocaleStatus.ASSET_LOCALE_IS_EMPTY; } } } - Log.d(TAG, "getAppSupportedLocales(). status: " + localeStatus + Log.d(TAG, "getAppSupportedLocales(). package: " + packageName + + ", status: " + localeStatus + ", appSupportedLocales:" + appSupportedLocales.size()); return new AppLocaleResult(localeStatus, appSupportedLocales); } - private static String[] getAssetLocales(Context context, String packageName) { + private static HashSet<Locale> getAssetLocales(Context context, String packageName) { + HashSet<Locale> result = new HashSet<>(); try { PackageManager packageManager = context.getPackageManager(); String[] locales = packageManager.getResourcesForApplication( @@ -78,16 +93,59 @@ class AppLocaleStore { .applicationInfo).getAssets().getNonSystemLocales(); if (locales == null) { Log.i(TAG, "[" + packageName + "] locales are null."); - return new String[0]; } else if (locales.length <= 0) { Log.i(TAG, "[" + packageName + "] locales length is 0."); - return new String[0]; + } else { + for (String language : locales) { + result.add(Locale.forLanguageTag(language)); + } } - return locales; } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Can not found the package name : " + packageName + " / " + e); } - return new String[0]; + return result; + } + + private static HashSet<Locale> filterNotMatchingLocale( + HashSet<Locale> appSupportedLocales, HashSet<Locale> assetLocale) { + return appSupportedLocales.stream() + .filter(locale -> matchLanguageInSet(locale, assetLocale)) + .collect(Collectors.toCollection(HashSet::new)); + } + + private static boolean matchLanguageInSet(Locale locale, HashSet<Locale> localesSet) { + if (localesSet.contains(locale)) { + return true; + } + for (Locale l: localesSet) { + if(LocaleList.matchesLanguageAndScript(l, locale)) { + return true; + } + } + return false; + } + + private static boolean hasInstallerInfo(Context context, String packageName) { + InstallSourceInfo installSourceInfo; + try { + installSourceInfo = context.getPackageManager().getInstallSourceInfo(packageName); + return installSourceInfo != null; + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Installer info not found for: " + packageName); + } + return false; + } + + private static boolean isSystemApp(Context context, String packageName) { + ApplicationInfo applicationInfo; + try { + applicationInfo = context.getPackageManager() + .getApplicationInfoAsUser(packageName, /* flags= */ 0, context.getUserId()); + return applicationInfo.isSystemApp(); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Application info not found for: " + packageName); + } + return false; } static class AppLocaleResult { @@ -100,9 +158,9 @@ class AppLocaleStore { } LocaleStatus mLocaleStatus; - ArrayList<Locale> mAppSupportedLocales; + HashSet<Locale> mAppSupportedLocales; - public AppLocaleResult(LocaleStatus localeStatus, ArrayList<Locale> appSupportedLocales) { + public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) { this.mLocaleStatus = localeStatus; this.mAppSupportedLocales = appSupportedLocales; } diff --git a/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java b/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java index 4f1f380c3a77..3e1b5f087c10 100644 --- a/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java +++ b/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java @@ -269,7 +269,7 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment protected CharSequence getItemLabel(DisplayResolveInfo dri) { final PackageManager pm = getContext().getPackageManager(); return getPinLabel(isPinned(dri), - isShortcutTarget() ? "" : dri.getResolveInfo().loadLabel(pm)); + isShortcutTarget() ? mShortcutTitle : dri.getResolveInfo().loadLabel(pm)); } @Nullable diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java index 153a7b4c514b..965895f08d6e 100644 --- a/core/java/com/android/internal/app/LocalePickerWithRegion.java +++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java @@ -36,7 +36,6 @@ import android.widget.SearchView; import com.android.internal.R; -import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Locale; @@ -204,13 +203,20 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O } private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotSupportedInApp( - boolean shouldShowList, ArrayList<Locale> supportedLocales) { + boolean shouldShowList, HashSet<Locale> supportedLocales) { Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); - if (shouldShowList) { - for(LocaleStore.LocaleInfo li: mLocaleList) { + if (!shouldShowList) { + return filteredList; + } + + for(LocaleStore.LocaleInfo li: mLocaleList) { + if (supportedLocales.contains(li.getLocale())) { + filteredList.add(li); + } else { for(Locale l: supportedLocales) { if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { filteredList.add(li); + break; } } } @@ -334,7 +340,7 @@ public class LocalePickerWithRegion extends ListFragment implements SearchView.O inflater.inflate(R.menu.language_selection_list, menu); final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu); - if (!mAppPackageName.isEmpty() && mOnActionExpandListener != null) { + if (!TextUtils.isEmpty(mAppPackageName) && mOnActionExpandListener != null) { searchMenuItem.setOnActionExpandListener(mOnActionExpandListener); } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index bd5a73d8194a..7e53a5afc315 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -119,6 +119,11 @@ public class ResolverActivity extends Activity implements @UnsupportedAppUsage public ResolverActivity() { + mIsIntentPicker = getClass().equals(ResolverActivity.class); + } + + protected ResolverActivity(boolean isIntentPicker) { + mIsIntentPicker = isIntentPicker; } private boolean mSafeForwardingMode; @@ -135,6 +140,8 @@ public class ResolverActivity extends Activity implements private String mReferrerPackage; private CharSequence mTitle; private int mDefaultTitleResId; + // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity. + private final boolean mIsIntentPicker; // Whether or not this activity supports choosing a default handler for the intent. @VisibleForTesting @@ -445,10 +452,6 @@ public class ResolverActivity extends Activity implements + (categories != null ? Arrays.toString(categories.toArray()) : "")); } - private boolean isIntentPicker() { - return getClass().equals(ResolverActivity.class); - } - protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( Intent[] initialIntents, List<ResolveInfo> rList, @@ -637,6 +640,11 @@ public class ResolverActivity extends Activity implements resetButtonBar(); + if (shouldUseMiniResolver()) { + View buttonContainer = findViewById(R.id.button_bar_container); + buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom); + } + // Need extra padding so the list can fully scroll up if (shouldAddFooterView()) { applyFooterView(mSystemWindowInsets.bottom); @@ -649,7 +657,8 @@ public class ResolverActivity extends Activity implements public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); - if (isIntentPicker() && shouldShowTabs() && !useLayoutWithDefault()) { + if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault() + && !shouldUseMiniResolver()) { updateIntentPickerPaddings(); } @@ -1084,7 +1093,7 @@ public class ResolverActivity extends Activity implements if (isAutolaunching()) { return; } - if (isIntentPicker()) { + if (mIsIntentPicker) { ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) .setUseLayoutWithDefault(useLayoutWithDefault()); } @@ -1108,7 +1117,7 @@ public class ResolverActivity extends Activity implements protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) { final ItemClickListener listener = new ItemClickListener(); setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener); - if (shouldShowTabs() && isIntentPicker()) { + if (shouldShowTabs() && mIsIntentPicker) { final ResolverDrawerLayout rdl = findViewById(R.id.contentPanel); if (rdl != null) { rdl.setMaxCollapsedHeight(getResources() @@ -1448,6 +1457,12 @@ public class ResolverActivity extends Activity implements return postRebuildList(rebuildCompleted); } + /** + * Mini resolver is shown when the user is choosing between browser[s] in this profile and a + * single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon + * and asks the user if they'd like to open that cross-profile app or use the in-profile + * browser. + */ private void configureMiniResolverContent() { mLayoutId = R.layout.miniresolver; setContentView(mLayoutId); @@ -1456,10 +1471,14 @@ public class ResolverActivity extends Activity implements mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0); boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK; - DisplayResolveInfo otherProfileResolveInfo = - mMultiProfilePagerAdapter.getInactiveListAdapter().mDisplayList.get(0); + ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter(); + DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0); + + // Load the icon asynchronously ImageView icon = findViewById(R.id.icon); - // TODO: Set icon drawable to app icon. + ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask( + otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon)); + iconTask.execute(); ((TextView) findViewById(R.id.open_cross_profile)).setText( getResources().getString( @@ -1479,12 +1498,20 @@ public class ResolverActivity extends Activity implements prepareIntentForCrossProfileLaunch(intent); } safelyStartActivityAsUser(otherProfileResolveInfo, - mMultiProfilePagerAdapter.getInactiveListAdapter().mResolverListController - .getUserHandle()); + inactiveAdapter.mResolverListController.getUserHandle()); }); } + /** + * Mini resolver should be used when all of the following are true: + * 1. This is the intent picker (ResolverActivity). + * 2. This profile only has web browser matches. + * 3. The other profile has a single non-browser match. + */ private boolean shouldUseMiniResolver() { + if (!mIsIntentPicker) { + return false; + } if (mMultiProfilePagerAdapter.getActiveListAdapter() == null || mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { return false; @@ -1790,7 +1817,7 @@ public class ResolverActivity extends Activity implements void onHorizontalSwipeStateChanged(int state) {} private void maybeHideDivider() { - if (!isIntentPicker()) { + if (!mIsIntentPicker) { return; } final View divider = findViewById(R.id.divider); @@ -1807,7 +1834,7 @@ public class ResolverActivity extends Activity implements protected void onProfileTabSelected() { } private void resetCheckedItem() { - if (!isIntentPicker()) { + if (!mIsIntentPicker) { return; } mLastSelected = ListView.INVALID_POSITION; diff --git a/core/java/com/android/internal/app/procstats/ProcessStats.java b/core/java/com/android/internal/app/procstats/ProcessStats.java index b7abe73a79eb..9fea86e837ca 100644 --- a/core/java/com/android/internal/app/procstats/ProcessStats.java +++ b/core/java/com/android/internal/app/procstats/ProcessStats.java @@ -374,14 +374,12 @@ public final class ProcessStats implements Parcelable { } } thisProc.add(otherProc); - if (thisProc.isActive()) { - UidState uidState = mUidStates.get(uid); - if (uidState == null) { - uidState = new UidState(this, uid); - mUidStates.put(uid, uidState); - } - uidState.addProcess(thisProc); + UidState uidState = mUidStates.get(uid); + if (uidState == null) { + uidState = new UidState(this, uid); + mUidStates.put(uid, uidState); } + uidState.addProcess(thisProc); } } @@ -1185,9 +1183,7 @@ public final class ProcessStats implements Parcelable { + " " + proc); mProcesses.put(procName, uid, proc); - if (proc.isActive()) { - mUidStates.get(uid).addProcess(proc); - } + mUidStates.get(uid).addProcess(proc); } } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index b03a8cbeb79c..6829f3da5717 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -644,7 +644,7 @@ public class BatteryStatsImpl extends BatteryStats { /** Schedule removal of UIDs corresponding to a removed user */ Future<?> scheduleCleanupDueToRemovedUser(int userId); /** Schedule a sync because of a process state change */ - Future<?> scheduleSyncDueToProcessStateChange(long delayMillis); + void scheduleSyncDueToProcessStateChange(int flags, long delayMillis); } public Handler mHandler; @@ -6215,9 +6215,7 @@ public class BatteryStatsImpl extends BatteryStats { long elapsedRealtimeMs, long uptimeMs) { if (mMobileRadioPowerState != powerState) { long realElapsedRealtimeMs; - final boolean active = - powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM - || powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH; + final boolean active = isActiveRadioPowerState(powerState); if (active) { if (uid > 0) { noteMobileRadioApWakeupLocked(elapsedRealtimeMs, uptimeMs, uid); @@ -6259,6 +6257,11 @@ public class BatteryStatsImpl extends BatteryStats { return false; } + private static boolean isActiveRadioPowerState(int powerState) { + return powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM + || powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH; + } + @GuardedBy("this") public void notePowerSaveModeLocked(boolean enabled) { notePowerSaveModeLocked(enabled, mClock.elapsedRealtime(), mClock.uptimeMillis()); @@ -12042,7 +12045,13 @@ public class BatteryStatsImpl extends BatteryStats { return; } - mBsi.mExternalSync.scheduleSyncDueToProcessStateChange( + int flags = ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE; + // Skip querying for inactive radio, where power usage is probably negligible. + if (!BatteryStatsImpl.isActiveRadioPowerState(mBsi.mMobileRadioPowerState)) { + flags &= ~ExternalStatsSync.UPDATE_RADIO; + } + + mBsi.mExternalSync.scheduleSyncDueToProcessStateChange(flags, mBsi.mConstants.PROC_STATE_CHANGE_COLLECTION_DELAY_MS); } diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java index f2c27a494fc9..d842eb6c4d49 100644 --- a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java +++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java @@ -395,7 +395,7 @@ public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCa @SyncAndDrawResult public int renderView(View view, Rect sourceRect) { HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest(); - request.setVsyncTime(SystemClock.elapsedRealtimeNanos()); + request.setVsyncTime(System.nanoTime()); if (updateForView(view)) { setupLighting(view); } diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index c769da57eecc..4044c579a57f 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -1921,6 +1921,7 @@ static void nativeRemoveCurrentInputFocus(JNIEnv* env, jclass clazz, jlong trans FocusRequest request; request.timestamp = systemTime(SYSTEM_TIME_MONOTONIC); request.displayId = displayId; + request.windowName = "<null>"; transaction->setFocusedWindow(request); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 0f328b034f38..f20b824e6bf7 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6441,11 +6441,11 @@ <!-- @SystemApi Must be required by a safety source to send an update using the {@link android.safetycenter.SafetyCenterManager}. - <p>Protection level: signature|privileged + <p>Protection level: internal|privileged @hide --> <permission android:name="android.permission.SEND_SAFETY_CENTER_UPDATE" - android:protectionLevel="signature|privileged" /> + android:protectionLevel="internal|privileged" /> <!-- @SystemApi Allows an application to launch device manager setup screens. <p>Not for use by third-party applications. diff --git a/core/res/res/anim-ldrtl/task_fragment_close_enter.xml b/core/res/res/anim-ldrtl/task_fragment_close_enter.xml new file mode 100644 index 000000000000..e5f5707c9a20 --- /dev/null +++ b/core/res/res/anim-ldrtl/task_fragment_close_enter.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + + <alpha + android:fromAlpha="1.0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/linear" + android:startOffset="0" + android:duration="200" /> + + <translate + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/fast_out_extra_slow_in" + android:startOffset="0" + android:duration="200" /> +</set>
\ No newline at end of file diff --git a/core/res/res/anim-ldrtl/task_fragment_close_exit.xml b/core/res/res/anim-ldrtl/task_fragment_close_exit.xml new file mode 100644 index 000000000000..c5a36542cc7d --- /dev/null +++ b/core/res/res/anim-ldrtl/task_fragment_close_exit.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + + <alpha + android:fromAlpha="1.0" + android:toAlpha="0.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/linear" + android:startOffset="35" + android:duration="83" /> + + <translate + android:fromXDelta="0" + android:toXDelta="-10%" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/fast_out_extra_slow_in" + android:startOffset="0" + android:duration="200" /> +</set>
\ No newline at end of file diff --git a/core/res/res/anim-ldrtl/task_fragment_open_enter.xml b/core/res/res/anim-ldrtl/task_fragment_open_enter.xml new file mode 100644 index 000000000000..b6f1af3e7840 --- /dev/null +++ b/core/res/res/anim-ldrtl/task_fragment_open_enter.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +/* +** Copyright 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. +*/ +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + + <alpha + android:fromAlpha="0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/linear" + android:startOffset="50" + android:duration="83" /> + + <translate + android:fromXDelta="-10%" + android:toXDelta="0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/fast_out_extra_slow_in" + android:duration="450" /> +</set>
\ No newline at end of file diff --git a/core/res/res/anim-ldrtl/task_fragment_open_exit.xml b/core/res/res/anim-ldrtl/task_fragment_open_exit.xml new file mode 100644 index 000000000000..6cea53c3e934 --- /dev/null +++ b/core/res/res/anim-ldrtl/task_fragment_open_exit.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?><!-- +/* +** Copyright 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. +*/ +--> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + + <alpha + android:fromAlpha="1.0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/standard_accelerate" + android:startOffset="0" + android:duration="450" /> + + <translate + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/fast_out_extra_slow_in" + android:duration="450" /> +</set>
\ No newline at end of file diff --git a/core/res/res/anim/task_fragment_close_enter.xml b/core/res/res/anim/task_fragment_close_enter.xml index c940552d53ad..cb6cdbeb71f4 100644 --- a/core/res/res/anim/task_fragment_close_enter.xml +++ b/core/res/res/anim/task_fragment_close_enter.xml @@ -17,16 +17,21 @@ <set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false"> - <scale - android:fromXScale="1.1" - android:toXScale="1" - android:fromYScale="1.1" - android:toYScale="1" - android:pivotX="50%" - android:pivotY="50%" + <alpha + android:fromAlpha="1.0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/linear" + android:startOffset="0" + android:duration="200" /> + + <translate android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:interpolator="@interpolator/fast_out_extra_slow_in" - android:duration="400"/> + android:startOffset="0" + android:duration="200" /> </set>
\ No newline at end of file diff --git a/core/res/res/anim/task_fragment_close_exit.xml b/core/res/res/anim/task_fragment_close_exit.xml index 8998f764ff9b..84d8b7e6b5cd 100644 --- a/core/res/res/anim/task_fragment_close_exit.xml +++ b/core/res/res/anim/task_fragment_close_exit.xml @@ -19,24 +19,21 @@ android:shareInterpolator="false" android:zAdjustment="top"> <alpha - android:fromAlpha="1" + android:fromAlpha="1.0" android:toAlpha="0.0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:interpolator="@interpolator/linear" - android:startOffset="33" - android:duration="50"/> - <scale - android:fromXScale="1" - android:toXScale="0.9" - android:fromYScale="1" - android:toYScale="0.9" - android:pivotX="50%" - android:pivotY="50%" + android:startOffset="0" + android:duration="83" /> + + <translate + android:fromXDelta="0" + android:toXDelta="10%" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:interpolator="@interpolator/fast_out_extra_slow_in" - android:duration="400"/> + android:duration="200" /> </set> diff --git a/core/res/res/anim/task_fragment_open_enter.xml b/core/res/res/anim/task_fragment_open_enter.xml index 6bc47deb2de4..aa61e6f17070 100644 --- a/core/res/res/anim/task_fragment_open_enter.xml +++ b/core/res/res/anim/task_fragment_open_enter.xml @@ -25,17 +25,13 @@ android:fillAfter="true" android:interpolator="@interpolator/linear" android:startOffset="50" - android:duration="50"/> - <scale - android:fromXScale="0.85" - android:toXScale="1" - android:fromYScale="0.85" - android:toYScale="1" - android:pivotX="50%" - android:pivotY="50%" + android:duration="83" /> + <translate + android:fromXDelta="10%" + android:toXDelta="0" android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:interpolator="@interpolator/fast_out_extra_slow_in" - android:duration="400"/> + android:duration="400" /> </set> diff --git a/core/res/res/anim/task_fragment_open_exit.xml b/core/res/res/anim/task_fragment_open_exit.xml index 160eb84223da..b4914d21d097 100644 --- a/core/res/res/anim/task_fragment_open_exit.xml +++ b/core/res/res/anim/task_fragment_open_exit.xml @@ -17,16 +17,20 @@ <set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false"> - <scale - android:fromXScale="1" - android:toXScale="1.05" - android:fromYScale="1" - android:toYScale="1.05" - android:pivotX="50%" - android:pivotY="50%" + <alpha + android:fromAlpha="1.0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@interpolator/standard_accelerate" + android:startOffset="0" + android:duration="400" /> + + <translate android:fillEnabled="true" android:fillBefore="true" android:fillAfter="true" android:interpolator="@interpolator/fast_out_extra_slow_in" - android:duration="400"/> + android:duration="400" /> </set>
\ No newline at end of file diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml index 44ed6f2a0676..85fe2835fd5e 100644 --- a/core/res/res/layout/miniresolver.xml +++ b/core/res/res/layout/miniresolver.xml @@ -24,12 +24,11 @@ android:id="@id/contentPanel"> <RelativeLayout - android:id="@+id/title_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alwaysShow="true" android:elevation="@dimen/resolver_elevation" - android:paddingTop="@dimen/resolver_small_margin" + android:paddingTop="24dp" android:paddingStart="@dimen/resolver_edge_margin" android:paddingEnd="@dimen/resolver_edge_margin" android:paddingBottom="@dimen/resolver_title_padding_bottom" @@ -47,8 +46,12 @@ android:id="@+id/open_cross_profile" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingTop="16dp" android:layout_below="@id/icon" android:layout_centerHorizontal="true" + android:textSize="24sp" + android:lineHeight="32sp" + android:gravity="center" android:textColor="?android:textColorPrimary" /> </RelativeLayout> @@ -58,15 +61,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alwaysShow="true" + android:paddingTop="32dp" + android:paddingBottom="@dimen/resolver_button_bar_spacing" android:orientation="vertical" android:background="?attr/colorBackground" android:layout_ignoreOffset="true"> - <View - android:id="@+id/resolver_button_bar_divider" - android:layout_width="match_parent" - android:layout_height="1dp" - android:background="?attr/colorBackground" - android:foreground="?attr/dividerVertical" /> <RelativeLayout style="?attr/buttonBarStyle" android:layout_width="match_parent" @@ -77,7 +76,6 @@ android:orientation="horizontal" android:layoutDirection="locale" android:measureWithLargestChild="true" - android:paddingTop="@dimen/resolver_button_bar_spacing" android:paddingBottom="@dimen/resolver_button_bar_spacing" android:paddingStart="@dimen/resolver_edge_margin" android:paddingEnd="@dimen/resolver_small_margin" diff --git a/core/res/res/values-mcc262/config.xml b/core/res/res/values-mcc262/config.xml index 79eefb7cecbe..d234061c8fda 100644 --- a/core/res/res/values-mcc262/config.xml +++ b/core/res/res/values-mcc262/config.xml @@ -21,5 +21,5 @@ for different hardware and product builds. --> <resources> <!-- Set to false to disable emergency alert. --> - <bool name="config_cellBroadcastAppLinks">false</bool> + <bool name="config_cellBroadcastAppLinks">true</bool> </resources>
\ No newline at end of file diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 5df3dde82b3e..b515abc4000f 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -448,5 +448,6 @@ <color name="accessibility_color_inversion_background">#546E7A</color> <!-- Color of camera light when camera is in use --> - <color name="camera_privacy_light">#FFFFFF</color> + <color name="camera_privacy_light_day">#FFFFFF</color> + <color name="camera_privacy_light_night">#FFFFFF</color> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 689ff66a3b4d..178bfbc80e5f 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1869,6 +1869,11 @@ --> <string name="config_defaultSearchSelectorPackageName" translatable="false"></string> + <!-- The package name of the default captive portal login app. Must be granted the + POST_NOTIFICATIONS permission. + --> + <string name="config_defaultCaptivePortalLoginPackageName" translatable="false"></string> + <!-- Whether to enable geocoder overlay which allows geocoder to be replaced by an app at run-time. When disabled, only the config_geocoderProviderPackageName package will be searched for @@ -2998,12 +3003,6 @@ </string-array> - <!-- When migrating notification settings into the permission framework, whether all existing - apps should be marked as 'user-set' (true) or whether only the apps that have explicitly - modified notification settings should be marked as 'user-set' (false). Users will not see - system generated permission prompts for 'user-set' apps. --> - <bool name="config_notificationForceUserSetOnUpgrade">true</bool> - <!-- Default Gravity setting for the system Toast view. Equivalent to: Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM --> <integer name="config_toastDefaultGravity">0x00000051</integer> @@ -5662,6 +5661,9 @@ <!-- Whether or not to enable the lock screen entry point for the QR code scanner. --> <bool name="config_enableQrCodeScannerOnLockScreen">false</bool> + <!-- Default component for QR code scanner --> + <string name="config_defaultQrCodeComponent"></string> + <!-- Whether Low Power Standby is supported and can be enabled. --> <bool name="config_lowPowerStandbySupported">false</bool> @@ -5752,6 +5754,14 @@ --> <integer name="config_bg_current_drain_location_min_duration">1800</integer> + <!-- The behavior when the system detects it has abusive current drains, whether or not to + move the app to the restricted standby bucket level. + True - we'll move the app to restricted standby bucket as long as its bg battery usage + goes beyond the threshold, False - we'll not move it. + Note: This should be only enabled on devices with high confidence on power measurement. + --> + <bool name="config_bg_current_drain_auto_restrict_abusive_apps">false</bool> + <!-- The behavior for an app with a FGS and its notification is still showing, when the system detects it's abusive and should be put into bg restricted level. True - we'll show the prompt to user, False - we'll not show it. @@ -5787,4 +5797,17 @@ <!-- List of the labels of requestable device state config values --> <string-array name="config_deviceStatesAvailableForAppRequests"/> + + <!-- Interval in milliseconds to average light sensor values for camera light brightness --> + <integer name="config_cameraPrivacyLightAlsAveragingIntervalMillis">3000</integer> + <!-- Light sensor's lux value to use as the threshold between using day or night brightness --> + <integer name="config_cameraPrivacyLightAlsNightThreshold">4</integer> + + <!-- List of system components which are allowed to receive ServiceState entries in an + un-sanitized form, even if the location toggle is off. This is intended ONLY for system + components, such as the telephony stack, which require access to the full ServiceState for + tasks such as network registration. --> + <string-array name="config_serviceStateLocationAllowedPackages"> + <item>"com.android.phone"</item> + </string-array> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 824dd8be0160..e5d90f00f327 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -876,9 +876,9 @@ <string name="permgroupdesc_sms">send and view SMS messages</string> <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. --> - <string name="permgrouplab_storage">Files and documents</string> + <string name="permgrouplab_storage">Files</string> <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. --> - <string name="permgroupdesc_storage">access files and documents on your device</string> + <string name="permgroupdesc_storage">access files on your device</string> <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]--> <string name="permgrouplab_readMediaAural">Music and audio</string> @@ -6313,5 +6313,5 @@ ul.</string> <string name="vdm_camera_access_denied" product="tablet">Can’t access the tablet’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string> <!-- Title for preference of the system default locale. [CHAR LIMIT=50]--> - <string name="system_locale_title">System language</string> + <string name="system_locale_title">System default</string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 443f9a628a7e..dcd706c15ff0 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3419,6 +3419,9 @@ <!-- Search Selector --> <java-symbol type="string" name="config_defaultSearchSelectorPackageName" /> + <!-- Captive Portal Login --> + <java-symbol type="string" name="config_defaultCaptivePortalLoginPackageName" /> + <!-- Optional IPsec algorithms --> <java-symbol type="array" name="config_optionalIpSecAlgorithms" /> @@ -4708,6 +4711,7 @@ <java-symbol type="string" name="config_wearSysUiPackage"/> <java-symbol type="string" name="config_wearSysUiMainActivity"/> + <java-symbol type="string" name="config_defaultQrCodeComponent"/> <java-symbol type="dimen" name="secondary_rounded_corner_radius" /> <java-symbol type="dimen" name="secondary_rounded_corner_radius_top" /> @@ -4753,7 +4757,10 @@ <!-- For VirtualDeviceManager --> <java-symbol type="string" name="vdm_camera_access_denied" /> - <java-symbol type="color" name="camera_privacy_light"/> + <java-symbol type="color" name="camera_privacy_light_day"/> + <java-symbol type="color" name="camera_privacy_light_night"/> + <java-symbol type="integer" name="config_cameraPrivacyLightAlsAveragingIntervalMillis"/> + <java-symbol type="integer" name="config_cameraPrivacyLightAlsNightThreshold"/> <java-symbol type="bool" name="config_bg_current_drain_monitor_enabled" /> <java-symbol type="array" name="config_bg_current_drain_threshold_to_restricted_bucket" /> @@ -4767,13 +4774,14 @@ <java-symbol type="array" name="config_bg_current_drain_high_threshold_to_bg_restricted" /> <java-symbol type="integer" name="config_bg_current_drain_media_playback_min_duration" /> <java-symbol type="integer" name="config_bg_current_drain_location_min_duration" /> + <java-symbol type="bool" name="config_bg_current_drain_auto_restrict_abusive_apps" /> <java-symbol type="bool" name="config_bg_prompt_fgs_with_noti_to_bg_restricted" /> <java-symbol type="bool" name="config_bg_prompt_abusive_apps_to_bg_restricted" /> <java-symbol type="integer" name="config_bg_current_drain_exempted_types" /> <java-symbol type="bool" name="config_bg_current_drain_high_threshold_by_bg_location" /> <java-symbol type="drawable" name="ic_swap_horiz" /> - <java-symbol type="bool" name="config_notificationForceUserSetOnUpgrade" /> <java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" /> + <java-symbol type="array" name="config_serviceStateLocationAllowedPackages" /> <!-- For app language picker --> <java-symbol type="string" name="system_locale_title" /> diff --git a/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java b/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java index 969357f66dde..22feecbd899f 100644 --- a/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java +++ b/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.platform.test.annotations.Presubmit; import android.util.ArraySet; -import org.junit.Ignore; import org.junit.Test; import java.util.Set; @@ -32,7 +31,6 @@ import java.util.Set; @Presubmit public class AppSearchShortcutInfoTest { - @Ignore("b/208375334") @Test public void testBuildShortcutAndGetValue() { final String category = @@ -51,7 +49,7 @@ public class AppSearchShortcutInfoTest { final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW); final ShortcutInfo shortcut = new AppSearchShortcutInfo.Builder(/*packageName=*/"", id) .setActivity(activity) - .setLongLabel(id) + .setShortLabel(id) .setIconResName(shortcutIconResName) .setIntent(shortcutIntent) .setPerson(person) @@ -64,11 +62,13 @@ public class AppSearchShortcutInfoTest { assertThat(shortcut.getId()).isEqualTo(id); assertThat(shortcut.getShortLabel()).isEqualTo(id); assertThat(shortcut.getIconResName()).isEqualTo(shortcutIconResName); - assertThat(shortcut.getIntent().toString()).isEqualTo(shortcut.toString()); + assertThat(shortcut.getIntent().toString()).isEqualTo(shortcutIntent.toString()); assertThat(shortcut.getPersons().length).isEqualTo(1); - assertThat(shortcut.getPersons()[0]).isEqualTo(person); + final Person target = shortcut.getPersons()[0]; + assertThat(target.getName()).isEqualTo(person.getName()); + assertThat(target.isBot()).isEqualTo(person.isBot()); + assertThat(target.isImportant()).isEqualTo(person.isImportant()); assertThat(shortcut.getCategories()).isEqualTo(categorySet); - assertThat(shortcut.getFlags()).isEqualTo(ShortcutInfo.FLAG_LONG_LIVED); assertThat(shortcut.getActivity()).isEqualTo(activity); } } diff --git a/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java b/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java index 786c22bf04f6..9b6bcda8aed0 100644 --- a/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java +++ b/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java @@ -19,17 +19,24 @@ package android.view; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.content.Context; +import android.widget.FrameLayout; import androidx.test.InstrumentationRegistry; import androidx.test.annotation.UiThreadTest; import androidx.test.filters.MediumTest; import androidx.test.rule.ActivityTestRule; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + @MediumTest public class RenderNodeAnimatorTest { @Rule @@ -57,4 +64,46 @@ public class RenderNodeAnimatorTest { anim.start(); // should initialize mTransformationInfo assertNotNull(view.mTransformationInfo); } + + @Test + public void testViewDetachCancelsRenderNodeAnimator() { + // Start a RenderNodeAnimator with a long duration time, then detach the target view + // before the animation completes. Detaching of a View from a window should force cancel all + // RenderNodeAnimators + CountDownLatch latch = new CountDownLatch(1); + + FrameLayout container = new FrameLayout(getContext()); + View view = new View(getContext()); + + getActivity().runOnUiThread(() -> { + container.addView(view, new FrameLayout.LayoutParams(100, 100)); + getActivity().setContentView(container); + }); + getActivity().runOnUiThread(() -> { + RenderNodeAnimator anim = new RenderNodeAnimator(0, 0, 10f, 30f); + anim.setDuration(10000); + anim.setTarget(view); + anim.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + latch.countDown(); + } + }); + + anim.start(); + }); + + getActivity().runOnUiThread(()-> { + container.removeView(view); + }); + + try { + Assert.assertTrue("onAnimationEnd not invoked", + latch.await(3000, TimeUnit.MILLISECONDS)); + } catch (InterruptedException excep) { + Assert.fail("Interrupted waiting for onAnimationEnd callback"); + } + } } diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java index 0c009a055e2b..4cf9c3fe75b9 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java @@ -39,6 +39,10 @@ public class ResolverWrapperActivity extends ResolverActivity { static final OverrideData sOverrides = new OverrideData(); private UsageStatsManager mUsm; + public ResolverWrapperActivity() { + super(/* isIntentPicker= */ true); + } + @Override public ResolverListAdapter createResolverListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java index 87c45dccfa0c..d19f9f5ea58f 100644 --- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java @@ -47,6 +47,7 @@ import android.telephony.DataConnectionRealTimeInfo; import android.telephony.ModemActivityInfo; import android.telephony.ServiceState; import android.telephony.TelephonyManager; +import android.util.MutableInt; import android.util.SparseIntArray; import android.util.SparseLongArray; import android.view.Display; @@ -1982,6 +1983,54 @@ public class BatteryStatsNoteTest extends TestCase { expectedTxDurationsMs, bi, state.currentTimeMs); } + @SmallTest + @SuppressWarnings("GuardedBy") + public void testProcStateSyncScheduling_mobileRadioActiveState() { + final MockClock clock = new MockClock(); // holds realtime and uptime in ms + final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock); + final MutableInt lastProcStateChangeFlags = new MutableInt(0); + + MockBatteryStatsImpl.DummyExternalStatsSync externalStatsSync = + new MockBatteryStatsImpl.DummyExternalStatsSync() { + @Override + public void scheduleSyncDueToProcessStateChange(int flags, + long delayMillis) { + lastProcStateChangeFlags.value = flags; + } + }; + + bi.setDummyExternalStatsSync(externalStatsSync); + + bi.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0); + + // Note mobile radio is on. + long curr = 1000L * (clock.realtime = clock.uptime = 1001); + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr, + UID); + + lastProcStateChangeFlags.value = 0; + clock.realtime = clock.uptime = 2002; + bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND); + + final int allProcFlags = BatteryStatsImpl.ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE; + assertEquals(allProcFlags, lastProcStateChangeFlags.value); + + // Note mobile radio is off. + curr = 1000L * (clock.realtime = clock.uptime = 3003); + bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, curr, + UID); + + lastProcStateChangeFlags.value = 0; + clock.realtime = clock.uptime = 4004; + bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_CACHED_EMPTY); + + final int noRadioProcFlags = BatteryStatsImpl.ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE + & ~BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO; + assertEquals( + "An inactive radio should not be queried on proc state change", + noRadioProcFlags, lastProcStateChangeFlags.value); + } + private void setFgState(int uid, boolean fgOn, MockBatteryStatsImpl bi) { // Note that noteUidProcessStateLocked uses ActivityManager process states. if (fgOn) { diff --git a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java index 00154a3d23b0..edeb5e9f4834 100644 --- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java +++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java @@ -212,7 +212,12 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { return flags; } - private class DummyExternalStatsSync implements ExternalStatsSync { + public void setDummyExternalStatsSync(DummyExternalStatsSync externalStatsSync) { + mExternalStatsSync = externalStatsSync; + setExternalStatsSyncLocked(mExternalStatsSync); + } + + public static class DummyExternalStatsSync implements ExternalStatsSync { public int flags = 0; @Override @@ -257,8 +262,7 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { } @Override - public Future<?> scheduleSyncDueToProcessStateChange(long delayMillis) { - return null; + public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) { } } } diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index ebf5832147f7..f030d80a3533 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -80,5 +80,6 @@ <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> <permission name="android.permission.READ_DEVICE_CONFIG" /> <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> + <permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" /> </privapp-permissions> </permissions> diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java index 5fd53adc409a..dadbd8d2d1aa 100644 --- a/graphics/java/android/graphics/RenderNode.java +++ b/graphics/java/android/graphics/RenderNode.java @@ -1611,6 +1611,11 @@ public final class RenderNode { nEndAllAnimators(mNativeRenderNode); } + /** @hide */ + public void forceEndAnimators() { + nForceEndAnimators(mNativeRenderNode); + } + /////////////////////////////////////////////////////////////////////////// // Regular JNI methods /////////////////////////////////////////////////////////////////////////// @@ -1633,6 +1638,8 @@ public final class RenderNode { private static native void nEndAllAnimators(long renderNode); + private static native void nForceEndAnimators(long renderNode); + /////////////////////////////////////////////////////////////////////////// // @CriticalNative methods /////////////////////////////////////////////////////////////////////////// diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java index 9ad6f3adbd33..6fff52a20062 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java @@ -206,6 +206,8 @@ class AndroidKeyStoreBCWorkaroundProvider extends Provider { putSignatureImpl("NONEwithECDSA", PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$NONE"); + putSignatureImpl("Ed25519", + PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$Ed25519"); putSignatureImpl("SHA1withECDSA", PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA1"); put("Alg.Alias.Signature.ECDSA", "SHA1withECDSA"); diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java index 8289671de212..5216a908826b 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java @@ -29,7 +29,10 @@ import libcore.util.EmptyArray; import java.io.ByteArrayOutputStream; import java.security.InvalidKeyException; import java.security.SignatureSpi; +import java.security.spec.NamedParameterSpec; +import java.util.Arrays; import java.util.List; +import java.util.Set; /** * Base class for {@link SignatureSpi} providing Android KeyStore backed ECDSA signatures. @@ -37,6 +40,10 @@ import java.util.List; * @hide */ abstract class AndroidKeyStoreECDSASignatureSpi extends AndroidKeyStoreSignatureSpiBase { + private static final Set<String> ACCEPTED_SIGNING_SCHEMES = Set.of( + KeyProperties.KEY_ALGORITHM_EC.toLowerCase(), + NamedParameterSpec.ED25519.getName().toLowerCase(), + "eddsa"); public final static class NONE extends AndroidKeyStoreECDSASignatureSpi { public NONE() { @@ -114,6 +121,18 @@ abstract class AndroidKeyStoreECDSASignatureSpi extends AndroidKeyStoreSignature } } + public static final class Ed25519 extends AndroidKeyStoreECDSASignatureSpi { + public Ed25519() { + // Ed25519 uses an internal digest system. + super(KeymasterDefs.KM_DIGEST_NONE); + } + + @Override + protected String getAlgorithm() { + return NamedParameterSpec.ED25519.getName(); + } + } + public final static class SHA1 extends AndroidKeyStoreECDSASignatureSpi { public SHA1() { super(KeymasterDefs.KM_DIGEST_SHA1); @@ -174,9 +193,10 @@ abstract class AndroidKeyStoreECDSASignatureSpi extends AndroidKeyStoreSignature @Override protected final void initKey(AndroidKeyStoreKey key) throws InvalidKeyException { - if (!KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(key.getAlgorithm())) { + if (!ACCEPTED_SIGNING_SCHEMES.contains(key.getAlgorithm().toLowerCase())) { throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm() - + ". Only" + KeyProperties.KEY_ALGORITHM_EC + " supported"); + + ". Only" + Arrays.toString(ACCEPTED_SIGNING_SCHEMES.stream().toArray()) + + " supported"); } long keySizeBits = -1; diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java index fc963a88c4d1..b1338d164055 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java @@ -61,6 +61,17 @@ public class AndroidKeyStoreKeyAgreementSpi extends KeyAgreementSpi } } + /** + * X25519 key agreement support. + * + * @hide + */ + public static class XDH extends AndroidKeyStoreKeyAgreementSpi { + public XDH() { + super(Algorithm.EC); + } + } + private final int mKeymintAlgorithm; // Fields below are populated by engineInit and should be preserved after engineDoFinal. diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java index 40659f5dbfb0..cdc1085a5015 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -712,7 +712,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE: throw new StrongBoxUnavailableException("Failed to generated key pair.", e); case ResponseCode.OUT_OF_KEYS: - throw makeOutOfKeysException(e, securityLevel); + return checkIfRetryableOrThrow(e, securityLevel); default: ProviderException p = new ProviderException("Failed to generate key pair.", e); if ((mSpec.getPurposes() & KeyProperties.PURPOSE_WRAP_KEY) != 0) { @@ -740,7 +740,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato // In case keystore reports OUT_OF_KEYS, call this handler in an attempt to remotely provision // some keys. - private ProviderException makeOutOfKeysException(KeyStoreException e, int securityLevel) { + GenerateKeyPairHelperResult checkIfRetryableOrThrow(KeyStoreException e, int securityLevel) { GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread .currentApplication()); KeyStoreException ksException; @@ -757,8 +757,11 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato rkpStatus = KeyStoreException.RKP_SERVER_REFUSED_ISSUANCE; break; case IGenerateRkpKeyService.Status.OK: - // This will actually retry once immediately, so on "OK" go ahead and return - // "temporarily unavailable". @see generateKeyPair + // Explicitly return not-OK here so we retry in generateKeyPair. All other cases + // should throw because a retry doesn't make sense if we didn't actually + // provision fresh keys. + return new GenerateKeyPairHelperResult( + KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE, null); case IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR: case IGenerateRkpKeyService.Status.HTTP_CLIENT_ERROR: case IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR: @@ -781,7 +784,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE); } ksException.initCause(e); - return new ProviderException("Failed to talk to RemoteProvisioner", ksException); + throw new ProviderException("Failed to provision new attestation keys.", ksException); } private void addAttestationParameters(@NonNull List<KeyParameter> params) diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java index 0355628b8135..9947d34495ab 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -104,6 +104,7 @@ public class AndroidKeyStoreProvider extends Provider { // javax.crypto.KeyAgreement put("KeyAgreement.ECDH", PACKAGE_NAME + ".AndroidKeyStoreKeyAgreementSpi$ECDH"); + put("KeyAgreement.XDH", PACKAGE_NAME + ".AndroidKeyStoreKeyAgreementSpi$XDH"); // java.security.SecretKeyFactory putSecretKeyFactoryImpl("AES"); @@ -235,8 +236,8 @@ public class AndroidKeyStoreProvider extends Provider { return new AndroidKeyStoreEdECPublicKey(descriptor, metadata, ED25519_OID, iSecurityLevel, publicKeyEncoded); } else if (X25519_ALIAS.equalsIgnoreCase(jcaKeyAlgorithm)) { - //TODO(b/214203951) missing classes in conscrypt - throw new ProviderException("Curve " + X25519_ALIAS + " not supported yet"); + return new AndroidKeyStoreXDHPublicKey(descriptor, metadata, X25519_ALIAS, + iSecurityLevel, publicKey.getEncoded()); } else { throw new ProviderException("Unsupported Android Keystore public key algorithm: " + jcaKeyAlgorithm); diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java new file mode 100644 index 000000000000..42589640d2b7 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java @@ -0,0 +1,47 @@ +/* + * 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.system.keystore2.Authorization; +import android.system.keystore2.KeyDescriptor; + +import java.security.PrivateKey; +import java.security.interfaces.EdECKey; +import java.security.spec.NamedParameterSpec; + +/** + * X25519 Private Key backed by Keystore. + * instance of {@link PrivateKey} and {@link EdECKey} + * + * @hide + */ +public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey { + public AndroidKeyStoreXDHPrivateKey( + @NonNull KeyDescriptor descriptor, long keyId, + @NonNull Authorization[] authorizations, + @NonNull String algorithm, + @NonNull KeyStoreSecurityLevel securityLevel) { + super(descriptor, keyId, authorizations, algorithm, securityLevel); + } + + @Override + public NamedParameterSpec getParams() { + return NamedParameterSpec.X25519; + } +} diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPublicKey.java new file mode 100644 index 000000000000..9f3df3d72d86 --- /dev/null +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPublicKey.java @@ -0,0 +1,120 @@ +/* + * 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 android.security.keystore2; + +import android.annotation.NonNull; +import android.security.KeyStoreSecurityLevel; +import android.system.keystore2.KeyDescriptor; +import android.system.keystore2.KeyMetadata; + +import java.math.BigInteger; +import java.security.interfaces.XECPublicKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.NamedParameterSpec; +import java.util.Arrays; + +/** + * {@link XECPublicKey} backed by keystore. + * This class re-implements Conscrypt's OpenSSLX25519PublicKey. The reason is that + * OpenSSLX25519PublicKey does not implement XECPublicKey and is not a part of Conscrypt's public + * interface so it cannot be referred to. + * + * So the functionality is duplicated here until (likely Android U) one of the things mentioned + * above is fixed. + * + * @hide + */ +public class AndroidKeyStoreXDHPublicKey extends AndroidKeyStorePublicKey implements XECPublicKey { + private static final byte[] X509_PREAMBLE = new byte[] { + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00, + }; + + private static final byte[] X509_PREAMBLE_WITH_NULL = new byte[] { + 0x30, 0x2C, 0x30, 0x07, 0x06, 0x03, 0x2B, 0x65, 0x6E, 0x05, 0x00, 0x03, 0x21, 0x00, + }; + + private static final int X25519_KEY_SIZE_BYTES = 32; + + private final byte[] mEncodedKey; + private final int mPreambleLength; + + public AndroidKeyStoreXDHPublicKey( + @NonNull KeyDescriptor descriptor, + @NonNull KeyMetadata metadata, + @NonNull String algorithm, + @NonNull KeyStoreSecurityLevel iSecurityLevel, + @NonNull byte[] encodedKey) { + super(descriptor, metadata, encodedKey, algorithm, iSecurityLevel); + mEncodedKey = encodedKey; + if (mEncodedKey == null) { + throw new IllegalArgumentException("empty encoded key."); + } + + mPreambleLength = matchesPreamble(X509_PREAMBLE, mEncodedKey) | matchesPreamble( + X509_PREAMBLE_WITH_NULL, mEncodedKey); + if (mPreambleLength == 0) { + throw new IllegalArgumentException("Key size is not correct size"); + } + } + + private static int matchesPreamble(byte[] preamble, byte[] encoded) { + if (encoded.length != (preamble.length + X25519_KEY_SIZE_BYTES)) { + return 0; + } + + if (Arrays.compare(preamble, 0, preamble.length, encoded, 0, preamble.length) != 0) { + return 0; + } + return preamble.length; + } + + @Override + AndroidKeyStorePrivateKey getPrivateKey() { + return new AndroidKeyStoreXDHPrivateKey( + getUserKeyDescriptor(), + getKeyIdDescriptor().nspace, + getAuthorizations(), + "x25519", + getSecurityLevel()); + } + + @Override + public BigInteger getU() { + return new BigInteger(Arrays.copyOfRange(mEncodedKey, mPreambleLength, mEncodedKey.length)); + } + + @Override + public byte[] getEncoded() { + return mEncodedKey.clone(); + } + + @Override + public String getAlgorithm() { + return "XDH"; + } + + @Override + public String getFormat() { + return "x.509"; + } + + @Override + public AlgorithmParameterSpec getParams() { + return NamedParameterSpec.X25519; + } +} + diff --git a/libs/WindowManager/Jetpack/src/TEST_MAPPING b/libs/WindowManager/Jetpack/src/TEST_MAPPING index eacfe2520a6a..f8f64001dd24 100644 --- a/libs/WindowManager/Jetpack/src/TEST_MAPPING +++ b/libs/WindowManager/Jetpack/src/TEST_MAPPING @@ -28,5 +28,10 @@ } ] } + ], + "imports": [ + { + "path": "vendor/google_testing/integration/tests/scenarios/src/android/platform/test/scenario/sysui" + } ] -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 9713c2735a9c..015205c7a063 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -32,6 +32,7 @@ import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.Instrumentation; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; @@ -234,13 +235,45 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) { // If the activity belongs to the current app process, we treat it as a new activity launch. - final Activity activity = ActivityThread.currentActivityThread().getActivity(activityToken); + final Activity activity = getActivity(activityToken); if (activity != null) { - onActivityCreated(activity); + // We don't allow split as primary for new launch because we currently only support + // launching to top. We allow split as primary for activity reparent because the + // activity may be split as primary before it is reparented out. In that case, we want + // to show it as primary again when it is reparented back. + if (!resolveActivityToContainer(activity, true /* canSplitAsPrimary */)) { + // When there is no embedding rule matched, try to place it in the top container + // like a normal launch. + placeActivityInTopContainer(activity); + } updateCallbackIfNecessary(); return; } - // TODO: handle for activity in other process. + + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.isInPictureInPicture()) { + // We don't embed activity when it is in PIP. + return; + } + + // If the activity belongs to a different app process, we treat it as starting new intent, + // since both actions might result in a new activity that should appear in an organized + // TaskFragment. + final WindowContainerTransaction wct = new WindowContainerTransaction(); + TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId, + activityIntent, null /* launchingActivity */); + if (targetContainer == null) { + // When there is no embedding rule matched, try to place it in the top container like a + // normal launch. + targetContainer = taskContainer.getTopTaskFragmentContainer(); + } + if (targetContainer == null) { + return; + } + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), activityToken); + mPresenter.applyTransaction(wct); + // Because the activity does not belong to the organizer process, we wait until + // onTaskFragmentAppeared to trigger updateCallbackIfNecessary(). } /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */ @@ -316,92 +349,244 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } + @VisibleForTesting void onActivityCreated(@NonNull Activity launchedActivity) { - handleActivityCreated(launchedActivity); + // TODO(b/229680885): we don't support launching into primary yet because we want to always + // launch the new activity on top. + resolveActivityToContainer(launchedActivity, false /* canSplitAsPrimary */); updateCallbackIfNecessary(); } /** - * Checks if the activity start should be routed to a particular container. It can create a new - * container for the activity and a new split container if necessary. + * Checks if the new added activity should be routed to a particular container. It can create a + * new container for the activity and a new split container if necessary. + * @param launchedActivity the new launched activity. + * @param canSplitAsPrimary whether we can put the new launched activity into primary split. + * @return {@code true} if the activity was placed in TaskFragment container. */ - // TODO(b/190433398): Break down into smaller functions. - void handleActivityCreated(@NonNull Activity launchedActivity) { - if (isInPictureInPicture(launchedActivity)) { - // We don't embed activity when it is in PIP. - return; + @VisibleForTesting + boolean resolveActivityToContainer(@NonNull Activity launchedActivity, + boolean canSplitAsPrimary) { + if (isInPictureInPicture(launchedActivity) || launchedActivity.isFinishing()) { + // We don't embed activity when it is in PIP, or finishing. Return true since we don't + // want any extra handling. + return true; } - final List<EmbeddingRule> splitRules = getSplitRules(); - final TaskFragmentContainer currentContainer = getContainerWithActivity( - launchedActivity.getActivityToken()); - - // Check if the activity is configured to always be expanded. - if (shouldExpand(launchedActivity, null, splitRules)) { - if (shouldContainerBeExpanded(currentContainer)) { - // Make sure that the existing container is expanded - mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); - } else { - // Put activity into a new expanded container - final TaskFragmentContainer newContainer = newContainer(launchedActivity, - launchedActivity.getTaskId()); - mPresenter.expandActivity(newContainer.getTaskFragmentToken(), - launchedActivity); - } - return; + + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new launched activity should always expand. + * 2. Whether the new launched activity should launch a placeholder. + * 3. Whether the new launched activity has already been in a split with a rule matched + * (likely done in #onStartActivity). + * 4. Whether the activity below (if any) should be split with the new launched activity. + * 5. Whether the activity split with the activity below (if any) should be split with the + * new launched activity. + */ + + // 1. Whether the new launched activity should always expand. + if (shouldExpand(launchedActivity, null /* intent */)) { + expandActivity(launchedActivity); + return true; } - // Check if activity requires a placeholder + // 2. Whether the new launched activity should launch a placeholder. if (launchPlaceholderIfNecessary(launchedActivity)) { + return true; + } + + // 3. Whether the new launched activity has already been in a split with a rule matched. + if (isNewActivityInSplitWithRuleMatched(launchedActivity)) { + return true; + } + + // 4. Whether the activity below (if any) should be split with the new launched activity. + final Activity activityBelow = findActivityBelow(launchedActivity); + if (activityBelow == null) { + // Can't find any activity below. + return false; + } + if (putActivitiesIntoSplitIfNecessary(activityBelow, launchedActivity)) { + // Have split rule of [ activityBelow | launchedActivity ]. + return true; + } + if (canSplitAsPrimary + && putActivitiesIntoSplitIfNecessary(launchedActivity, activityBelow)) { + // Have split rule of [ launchedActivity | activityBelow]. + return true; + } + + // 5. Whether the activity split with the activity below (if any) should be split with the + // new launched activity. + final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( + activityBelow); + final SplitContainer topSplit = getActiveSplitForContainer(activityBelowContainer); + if (topSplit == null || !isTopMostSplit(topSplit)) { + // Skip if it is not the topmost split. + return false; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == activityBelowContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity == null || otherTopActivity == launchedActivity) { + // Can't find the top activity on the other split TaskFragment. + return false; + } + if (putActivitiesIntoSplitIfNecessary(otherTopActivity, launchedActivity)) { + // Have split rule of [ otherTopActivity | launchedActivity ]. + return true; + } + // Have split rule of [ launchedActivity | otherTopActivity]. + return canSplitAsPrimary + && putActivitiesIntoSplitIfNecessary(launchedActivity, otherTopActivity); + } + + /** + * Places the given activity to the top most TaskFragment in the task if there is any. + */ + @VisibleForTesting + void placeActivityInTopContainer(@NonNull Activity activity) { + if (getContainerWithActivity(activity) != null) { + // The activity has already been put in a TaskFragment. This is likely to be done by + // the server when the activity is started. + return; + } + final int taskId = getTaskId(activity); + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null) { return; } + final TaskFragmentContainer targetContainer = taskContainer.getTopTaskFragmentContainer(); + if (targetContainer == null) { + return; + } + targetContainer.addPendingAppearedActivity(activity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), + activity.getActivityToken()); + mPresenter.applyTransaction(wct); + } + + /** + * Expands the given activity by either expanding the TaskFragment it is currently in or putting + * it into a new expanded TaskFragment. + */ + private void expandActivity(@NonNull Activity activity) { + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (shouldContainerBeExpanded(container)) { + // Make sure that the existing container is expanded. + mPresenter.expandTaskFragment(container.getTaskFragmentToken()); + } else { + // Put activity into a new expanded container. + final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); + mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity); + } + } + + /** Whether the given new launched activity is in a split with a rule matched. */ + private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) { + final TaskFragmentContainer container = getContainerWithActivity(launchedActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(container); + if (splitContainer == null) { + return false; + } - // TODO(b/190433398): Check if it is a placeholder and there is already another split - // created by the primary activity. This is necessary for the case when the primary activity - // launched another secondary in the split, but the placeholder was still launched by the - // logic above. We didn't prevent the placeholder launcher because we didn't know that - // another secondary activity is coming up. + if (container == splitContainer.getPrimaryContainer()) { + // The new launched can be in the primary container when it is starting a new activity + // onCreate, thus the secondary may still be empty. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + final Activity secondaryActivity = secondaryContainer.getTopNonFinishingActivity(); + return secondaryActivity == null + || getSplitRule(launchedActivity, secondaryActivity) != null; + } - // Check if the activity should form a split with the activity below in the same task - // fragment. + // Check if the new launched activity is a placeholder. + if (splitContainer.getSplitRule() instanceof SplitPlaceholderRule) { + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) splitContainer.getSplitRule(); + final ComponentName placeholderName = placeholderRule.getPlaceholderIntent() + .getComponent(); + // TODO(b/232330767): Do we have a better way to check this? + return placeholderName == null + || placeholderName.equals(launchedActivity.getComponentName()) + || placeholderRule.getPlaceholderIntent().equals(launchedActivity.getIntent()); + } + + // Check if the new launched activity should be split with the primary top activity. + final Activity primaryActivity = splitContainer.getPrimaryContainer() + .getTopNonFinishingActivity(); + if (primaryActivity == null) { + return false; + } + /* TODO(b/231845476) we should always respect clearTop. + final SplitPairRule curSplitRule = (SplitPairRule) splitContainer.getSplitRule(); + final SplitPairRule splitRule = getSplitRule(primaryActivity, launchedActivity); + return splitRule != null && haveSamePresentation(splitRule, curSplitRule) + // If the new launched split rule should clear top and it is not the bottom most, + // it means we should create a new split pair and clear the existing secondary. + && (!splitRule.shouldClearTop() + || container.getBottomMostActivity() == launchedActivity); + */ + return getSplitRule(primaryActivity, launchedActivity) != null; + } + + /** Finds the activity below the given activity. */ + @Nullable + private Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; - if (currentContainer != null) { - final List<Activity> containerActivities = currentContainer.collectActivities(); - final int index = containerActivities.indexOf(launchedActivity); + final TaskFragmentContainer container = getContainerWithActivity(activity); + if (container != null) { + final List<Activity> containerActivities = container.collectActivities(); + final int index = containerActivities.indexOf(activity); if (index > 0) { activityBelow = containerActivities.get(index - 1); } } if (activityBelow == null) { - IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( - launchedActivity.getActivityToken()); + final IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( + activity.getActivityToken()); if (belowToken != null) { - activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken); + activityBelow = getActivity(belowToken); } } - if (activityBelow == null) { - return; - } + return activityBelow; + } - // Check if the split is already set. - final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( - activityBelow.getActivityToken()); - if (currentContainer != null && activityBelowContainer != null) { - final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, - activityBelowContainer); - if (existingSplit != null) { - // There is already an active split with the activity below. - return; - } + /** + * Checks if there is a rule to split the two activities. If there is one, puts them into split + * and returns {@code true}. Otherwise, returns {@code false}. + */ + private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); + if (splitRule == null) { + return false; } - - final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, - splitRules); - if (splitPairRule == null) { - return; + final TaskFragmentContainer primaryContainer = getContainerWithActivity( + primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); + if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() + && canReuseContainer(splitRule, splitContainer.getSplitRule())) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); + if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { + // The activity is already in the target TaskFragment. + return true; + } + secondaryContainer.addPendingAppearedActivity(secondaryActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.reparentActivityToTaskFragment( + secondaryContainer.getTaskFragmentToken(), + secondaryActivity.getActivityToken()); + mPresenter.applyTransaction(wct); + return true; } - - mPresenter.createNewSplitContainer(activityBelow, launchedActivity, - splitPairRule); + // Create new split pair. + mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); + return true; } private void onActivityConfigurationChanged(@NonNull Activity activity) { @@ -409,8 +594,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // We don't embed activity when it is in PIP. return; } - final TaskFragmentContainer currentContainer = getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer currentContainer = getContainerWithActivity(activity); if (currentContainer != null) { // Changes to activities in controllers are handled in @@ -443,14 +627,149 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** + * When we are trying to handle a new activity Intent, returns the {@link TaskFragmentContainer} + * that we should reparent the new activity to if there is any embedding rule matched. + * + * @param wct {@link WindowContainerTransaction} including all the window change + * requests. The caller is responsible to call + * {@link android.window.TaskFragmentOrganizer#applyTransaction}. + * @param taskId The Task to start the activity in. + * @param intent The {@link Intent} for starting the new launched activity. + * @param launchingActivity The {@link Activity} that starts the new activity. We will + * prioritize to split the new activity with it if it is not + * {@code null}. + * @return the {@link TaskFragmentContainer} to start the new activity in. {@code null} if there + * is no embedding rule matched. + */ + @VisibleForTesting + @Nullable + TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, + int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { + /* + * We will check the following to see if there is any embedding rule matched: + * 1. Whether the new activity intent should always expand. + * 2. Whether the launching activity (if set) should be split with the new activity intent. + * 3. Whether the top activity (if any) should be split with the new activity intent. + * 4. Whether the top activity (if any) in other split should be split with the new + * activity intent. + */ + + // 1. Whether the new activity intent should always expand. + if (shouldExpand(null /* activity */, intent)) { + return createEmptyExpandedContainer(wct, taskId, launchingActivity); + } + + // 2. Whether the launching activity (if set) should be split with the new activity intent. + if (launchingActivity != null) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + launchingActivity, intent, true /* respectClearTop */); + if (container != null) { + return container; + } + } + + // 3. Whether the top activity (if any) should be split with the new activity intent. + final TaskContainer taskContainer = getTaskContainer(taskId); + if (taskContainer == null || taskContainer.getTopTaskFragmentContainer() == null) { + // There is no other activity in the Task to check split with. + return null; + } + final TaskFragmentContainer topContainer = taskContainer.getTopTaskFragmentContainer(); + final Activity topActivity = topContainer.getTopNonFinishingActivity(); + if (topActivity != null && topActivity != launchingActivity) { + final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, + topActivity, intent, false /* respectClearTop */); + if (container != null) { + return container; + } + } + + // 4. Whether the top activity (if any) in other split should be split with the new + // activity intent. + final SplitContainer topSplit = getActiveSplitForContainer(topContainer); + if (topSplit == null) { + return null; + } + final TaskFragmentContainer otherTopContainer = + topSplit.getPrimaryContainer() == topContainer + ? topSplit.getSecondaryContainer() + : topSplit.getPrimaryContainer(); + final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity(); + if (otherTopActivity != null && otherTopActivity != launchingActivity) { + return getSecondaryContainerForSplitIfAny(wct, otherTopActivity, intent, + false /* respectClearTop */); + } + return null; + } + + /** + * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into. + */ + @Nullable + private TaskFragmentContainer createEmptyExpandedContainer( + @NonNull WindowContainerTransaction wct, int taskId, + @Nullable Activity launchingActivity) { + // We need an activity in the organizer process in the same Task to use as the owner + // activity, as well as to get the Task window info. + final Activity activityInTask; + if (launchingActivity != null) { + activityInTask = launchingActivity; + } else { + final TaskContainer taskContainer = getTaskContainer(taskId); + activityInTask = taskContainer != null + ? taskContainer.getTopNonFinishingActivity() + : null; + } + if (activityInTask == null) { + // Can't find any activity in the Task that we can use as the owner activity. + return null; + } + final TaskFragmentContainer expandedContainer = newContainer(null /* activity */, + activityInTask, taskId); + mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(), + activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + return expandedContainer; + } + + /** + * Returns a container for the new activity intent to launch into as splitting with the primary + * activity. + */ + @Nullable + private TaskFragmentContainer getSecondaryContainerForSplitIfAny( + @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, + @NonNull Intent intent, boolean respectClearTop) { + final SplitPairRule splitRule = getSplitRule(primaryActivity, intent); + if (splitRule == null) { + return null; + } + final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity); + final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer); + if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() + && (canReuseContainer(splitRule, splitContainer.getSplitRule()) + // TODO(b/231845476) we should always respect clearTop. + || !respectClearTop)) { + // Can launch in the existing secondary container if the rules share the same + // presentation. + return splitContainer.getSecondaryContainer(); + } + // Create a new TaskFragment to split with the primary activity for the new activity. + return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, splitRule); + } + + /** * Returns a container that this activity is registered with. An activity can only belong to one * container, or no container at all. */ @Nullable - TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { + TaskFragmentContainer getContainerWithActivity(@NonNull Activity activity) { + final IBinder activityToken = activity.getActivityToken(); for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers; - for (TaskFragmentContainer container : containers) { + // Traverse from top to bottom in case an activity is added to top pending, and hasn't + // received update from server yet. + for (int j = containers.size() - 1; j >= 0; j--) { + final TaskFragmentContainer container = containers.get(j); if (container.hasActivity(activityToken)) { return container; } @@ -647,8 +966,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer == null) { return; } - final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; - if (splitContainer != splitContainers.get(splitContainers.size() - 1)) { + if (!isTopMostSplit(splitContainer)) { // Skip position update - it isn't the topmost split. return; } @@ -664,11 +982,21 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mPresenter.updateSplitContainer(splitContainer, container, wct); } + /** Whether the given split is the topmost split in the Task. */ + private boolean isTopMostSplit(@NonNull SplitContainer splitContainer) { + final List<SplitContainer> splitContainers = splitContainer.getPrimaryContainer() + .getTaskContainer().mSplitContainers; + return splitContainer == splitContainers.get(splitContainers.size() - 1); + } + /** * Returns the top active split container that has the provided container, if available. */ @Nullable - private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { + private SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { + if (container == null) { + return null; + } final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; if (splitContainers.isEmpty()) { return null; @@ -687,8 +1015,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns the active split that has the provided containers as primary and secondary or as * secondary and primary, if available. */ + @VisibleForTesting @Nullable - private SplitContainer getActiveSplitForContainers( + SplitContainer getActiveSplitForContainers( @NonNull TaskFragmentContainer firstContainer, @NonNull TaskFragmentContainer secondContainer) { final List<SplitContainer> splitContainers = firstContainer.getTaskContainer() @@ -718,15 +1047,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { - final TaskFragmentContainer container = getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer container = getContainerWithActivity(activity); // Don't launch placeholder if the container is occluded. if (container != null && container != getTopActiveContainer(container.getTaskId())) { return false; } - SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container) - : null; + final SplitContainer splitContainer = getActiveSplitForContainer(container); if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { // Don't launch placeholder in primary split container return false; @@ -861,14 +1188,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return false; } - final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers; - for (SplitContainer splitContainer : splitContainers) { - if (container.equals(splitContainer.getPrimaryContainer()) - || container.equals(splitContainer.getSecondaryContainer())) { - return false; - } - } - return true; + return getActiveSplitForContainer(container) == null; } /** @@ -876,9 +1196,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryActivityIntent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -894,9 +1214,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a split rule for the provided pair of primary and secondary activities if available. */ @Nullable - private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, - @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) { - for (EmbeddingRule rule : splitRules) { + private SplitPairRule getSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof SplitPairRule)) { continue; } @@ -933,16 +1253,24 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return mHandler; } + int getTaskId(@NonNull Activity activity) { + // Prefer to get the taskId from TaskFragmentContainer because Activity.getTaskId() is an + // IPC call. + final TaskFragmentContainer container = getContainerWithActivity(activity); + return container != null ? container.getTaskId() : activity.getTaskId(); + } + + @Nullable + Activity getActivity(@NonNull IBinder activityToken) { + return ActivityThread.currentActivityThread().getActivity(activityToken); + } + /** * Returns {@code true} if an Activity with the provided component name should always be * expanded to occupy full task bounds. Such activity must not be put in a split. */ - private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent, - List<EmbeddingRule> splitRules) { - if (splitRules == null) { - return false; - } - for (EmbeddingRule rule : splitRules) { + private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) { + for (EmbeddingRule rule : mSplitRules) { if (!(rule instanceof ActivityRule)) { continue; } @@ -996,8 +1324,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, @NonNull Activity associatedActivity) { - TaskFragmentContainer associatedContainer = getContainerWithActivity( - associatedActivity.getActivityToken()); + final TaskFragmentContainer associatedContainer = getContainerWithActivity( + associatedActivity); if (associatedContainer == null) { return false; } @@ -1085,130 +1413,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return super.onStartActivity(who, intent, options); } - if (shouldExpand(null, intent, getSplitRules())) { - setLaunchingInExpandedContainer(launchingActivity, options); - } else if (!splitWithLaunchingActivity(launchingActivity, intent, options)) { - setLaunchingInSameSideContainer(launchingActivity, intent, options); + final int taskId = getTaskId(launchingActivity); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct, + taskId, intent, launchingActivity); + if (launchedInTaskFragment != null) { + mPresenter.applyTransaction(wct); + // Amend the request to let the WM know that the activity should be placed in the + // dedicated container. + options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + launchedInTaskFragment.getTaskFragmentToken()); } return super.onStartActivity(who, intent, options); } - - private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) { - TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer( - launchingActivity); - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - newContainer.getTaskFragmentToken()); - } - - /** - * Returns {@code true} if the activity that is going to be started via the - * {@code intent} should be paired with the {@code launchingActivity} and is set to be - * launched in the side container. - */ - private boolean splitWithLaunchingActivity(Activity launchingActivity, Intent intent, - Bundle options) { - final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return false; - } - - // Check if there is any existing side container to launch into. - TaskFragmentContainer secondaryContainer = findSideContainerForNewLaunch( - launchingActivity, splitPairRule); - if (secondaryContainer == null) { - // Create a new split with an empty side container. - secondaryContainer = mPresenter - .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule); - } - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - secondaryContainer.getTaskFragmentToken()); - return true; - } - - /** - * Finds if there is an existing split side {@link TaskFragmentContainer} that can be used - * for the new rule. - */ - @Nullable - private TaskFragmentContainer findSideContainerForNewLaunch(Activity launchingActivity, - SplitPairRule splitPairRule) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return null; - } - - // We only check if the launching activity is the primary of the split. We will check - // if the launching activity is the secondary in #setLaunchingInSameSideContainer. - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null - || splitContainer.getPrimaryContainer() != launchingContainer) { - return null; - } - - if (canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return splitContainer.getSecondaryContainer(); - } - return null; - } - - /** - * Checks if the activity that is going to be started via the {@code intent} should be - * paired with the existing top activity which is currently paired with the - * {@code launchingActivity}. If so, set the activity to be launched in the same side - * container of the {@code launchingActivity}. - */ - private void setLaunchingInSameSideContainer(Activity launchingActivity, Intent intent, - Bundle options) { - final TaskFragmentContainer launchingContainer = getContainerWithActivity( - launchingActivity.getActivityToken()); - if (launchingContainer == null) { - return; - } - - final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); - if (splitContainer == null) { - return; - } - - if (splitContainer.getSecondaryContainer() != launchingContainer) { - return; - } - - // The launching activity is on the secondary container. Retrieve the primary - // activity from the other container. - Activity primaryActivity = - splitContainer.getPrimaryContainer().getTopNonFinishingActivity(); - if (primaryActivity == null) { - return; - } - - final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent, - getSplitRules()); - if (splitPairRule == null) { - return; - } - - // Can only launch in the same container if the rules share the same presentation. - if (!canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { - return; - } - - // Amend the request to let the WM know that the activity should be placed in the - // dedicated container. This is necessary for the case that the activity is started - // into a new Task, or new Task will be escaped from the current host Task and be - // displayed in fullscreen. - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, - launchingContainer.getTaskFragmentToken()); - } } /** @@ -1228,8 +1446,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { return false; } + return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2); + } + + /** Whether the two rules have the same presentation. */ + private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) { + // TODO(b/231655482): add util method to do the comparison in SplitPairRule. return rule1.getSplitRatio() == rule2.getSplitRatio() - && rule1.getLayoutDirection() == rule2.getLayoutDirection(); + && rule1.getLayoutDirection() == rule2.getLayoutDirection() + && rule1.getFinishPrimaryWithSecondary() + == rule2.getFinishPrimaryWithSecondary() + && rule1.getFinishSecondaryWithPrimary() + == rule2.getFinishSecondaryWithPrimary(); } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index b32f4fa67906..43d0402c1525 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -16,8 +16,6 @@ package androidx.window.extensions.embedding; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - import android.app.Activity; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -100,10 +98,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * Creates a new split with the primary activity and an empty secondary container. * @return The newly created secondary container. */ - TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity, + @NonNull + TaskFragmentContainer createNewSplitWithEmptySideContainer( + @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull SplitPairRule rule) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - final Rect parentBounds = getParentContainerBounds(primaryActivity); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, isLtr(primaryActivity, rule)); @@ -127,8 +125,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule); - applyTransaction(wct); - return secondaryContainer; } @@ -155,8 +151,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr(primaryActivity, rule)); + final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( + secondaryActivity); + TaskFragmentContainer containerToAvoid = primaryContainer; + if (rule.shouldClearTop() && curSecondaryContainer != null) { + // Do not reuse the current TaskFragment if the rule is to clear top. + containerToAvoid = curSecondaryContainer; + } final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, - secondaryActivity, secondaryRectBounds, primaryContainer); + secondaryActivity, secondaryRectBounds, containerToAvoid); // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); @@ -167,21 +170,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } /** - * Creates a new expanded container. - */ - TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) { - final TaskFragmentContainer newContainer = mController.newContainer(null /* activity */, - launchingActivity, launchingActivity.getTaskId()); - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - createTaskFragment(wct, newContainer.getTaskFragmentToken(), - launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); - - applyTransaction(wct); - return newContainer; - } - - /** * Creates a new container or resizes an existing container for activity to the provided bounds. * @param activity The activity to be re-parented to the container if necessary. * @param containerToAvoid Re-parent from this container if an activity is already in it. @@ -189,8 +177,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private TaskFragmentContainer prepareContainerForActivity( @NonNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { - TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + TaskFragmentContainer container = mController.getContainerWithActivity(activity); final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); if (container == null || container == containerToAvoid) { container = mController.newContainer(activity, taskId); @@ -230,7 +217,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { isLtr(launchingActivity, rule)); TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( - launchingActivity.getActivityToken()); + launchingActivity); if (primaryContainer == null) { primaryContainer = mController.newContainer(launchingActivity, launchingActivity.getTaskId()); @@ -460,8 +447,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull Rect getParentContainerBounds(@NonNull Activity activity) { - final TaskFragmentContainer container = mController.getContainerWithActivity( - activity.getActivityToken()); + final TaskFragmentContainer container = mController.getContainerWithActivity(activity); if (container != null) { return getParentContainerBounds(container); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index dba71ef21946..0ea5603b1f3d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -142,4 +142,23 @@ class TaskContainer { container.removePendingAppearedActivity(pendingAppearedActivity); } } + + @Nullable + TaskFragmentContainer getTopTaskFragmentContainer() { + if (mContainers.isEmpty()) { + return null; + } + return mContainers.get(mContainers.size() - 1); + } + + @Nullable + Activity getTopNonFinishingActivity() { + for (int i = mContainers.size() - 1; i >= 0; i--) { + final Activity activity = mContainers.get(i).getTopNonFinishingActivity(); + if (activity != null) { + return activity; + } + } + return null; + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java index b3becad3dc5a..cdee9e386b33 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -17,6 +17,8 @@ package androidx.window.extensions.embedding; import static android.graphics.Matrix.MSCALE_X; +import static android.graphics.Matrix.MTRANS_X; +import static android.graphics.Matrix.MTRANS_Y; import android.graphics.Rect; import android.view.Choreographer; @@ -96,22 +98,20 @@ class TaskFragmentAnimationAdapter { mTarget.localBounds.left, mTarget.localBounds.top); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); - - // Open/close animation may scale up the surface. Apply an inverse scale to the window crop - // so that it will not be covering other windows. - mVecs[1] = mVecs[2] = 0; - mVecs[0] = mVecs[3] = 1; - mTransformation.getMatrix().mapVectors(mVecs); - mVecs[0] = 1.f / mVecs[0]; - mVecs[3] = 1.f / mVecs[3]; - final Rect clipRect = mTarget.localBounds; - mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); - mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); - mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); - mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); - mRect.offsetTo(Math.round(mTarget.localBounds.width() * (1 - mVecs[0]) / 2.f), - Math.round(mTarget.localBounds.height() * (1 - mVecs[3]) / 2.f)); - t.setWindowCrop(mLeash, mRect); + // Get current animation position. + final int positionX = Math.round(mMatrix[MTRANS_X]); + final int positionY = Math.round(mMatrix[MTRANS_Y]); + // The exiting surface starts at position: mTarget.localBounds and moves with + // positionX varying. Offset our crop region by the amount we have slided so crop + // regions stays exactly on the original container in split. + final int cropOffsetX = mTarget.localBounds.left - positionX; + final int cropOffsetY = mTarget.localBounds.top - positionY; + final Rect cropRect = new Rect(); + cropRect.set(mTarget.localBounds); + // Because window crop uses absolute position. + cropRect.offsetTo(0, 0); + cropRect.offset(cropOffsetX, cropOffsetY); + t.setCrop(mLeash, cropRect); } /** Called after animation finished. */ diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 6693755ee102..26ddae4a0818 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Activity; -import android.app.ActivityThread; import android.app.WindowConfiguration.WindowingMode; import android.graphics.Rect; import android.os.Binder; @@ -122,21 +121,24 @@ class TaskFragmentContainer { /** List of activities that belong to this container and live in this process. */ @NonNull List<Activity> collectActivities() { + final List<Activity> allActivities = new ArrayList<>(); + if (mInfo != null) { + // Add activities reported from the server. + for (IBinder token : mInfo.getActivities()) { + final Activity activity = mController.getActivity(token); + if (activity != null && !activity.isFinishing()) { + allActivities.add(activity); + } + } + } + // Add the re-parenting activity, in case the server has not yet reported the task // fragment info update with it placed in this container. We still want to apply rules // in this intermediate state. - List<Activity> allActivities = new ArrayList<>(); - if (!mPendingAppearedActivities.isEmpty()) { - allActivities.addAll(mPendingAppearedActivities); - } - // Add activities reported from the server. - if (mInfo == null) { - return allActivities; - } - ActivityThread activityThread = ActivityThread.currentActivityThread(); - for (IBinder token : mInfo.getActivities()) { - Activity activity = activityThread.getActivity(token); - if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) { + // Place those on top of the list since they will be on the top after reported from the + // server. + for (Activity activity : mPendingAppearedActivities) { + if (!activity.isFinishing()) { allActivities.add(activity); } } @@ -243,6 +245,12 @@ class TaskFragmentContainer { return i >= 0 ? activities.get(i) : null; } + @Nullable + Activity getBottomMostActivity() { + final List<Activity> activities = collectActivities(); + return activities.isEmpty() ? null : activities.get(0); + } + boolean isEmpty() { return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty()); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 34cde9bca763..353c7df2cbc5 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -16,6 +16,9 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -24,23 +27,35 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import android.annotation.NonNull; import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.Rect; import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.Pair; import android.window.TaskFragmentInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -53,6 +68,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -67,8 +83,10 @@ import java.util.List; public class SplitControllerTest { private static final int TASK_ID = 10; private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200); + private static final float SPLIT_RATIO = 0.5f; + private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( + new ComponentName("test", "placeholder")); - @Mock private Activity mActivity; @Mock private Resources mActivityResources; @@ -89,12 +107,13 @@ public class SplitControllerTest { mSplitPresenter = mSplitController.mPresenter; spyOn(mSplitController); spyOn(mSplitPresenter); + doNothing().when(mSplitPresenter).applyTransaction(any()); final Configuration activityConfig = new Configuration(); activityConfig.windowConfiguration.setBounds(TASK_BOUNDS); activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS); - doReturn(mActivityResources).when(mActivity).getResources(); doReturn(activityConfig).when(mActivityResources).getConfiguration(); doReturn(mHandler).when(mSplitController).getHandler(); + mActivity = createMockActivity(); } @Test @@ -260,6 +279,622 @@ public class SplitControllerTest { mSplitController.updateContainer(mTransaction, tf); - verify(mSplitPresenter).updateSplitContainer(eq(splitContainer), eq(tf), eq(mTransaction)); + verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction); + } + + @Test + public void testOnActivityCreated() { + mSplitController.onActivityCreated(mActivity); + + // Disallow to split as primary because we want the new launch to be always on top. + verify(mSplitController).resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + } + + @Test + public void testOnActivityReparentToTask_sameProcess() { + mSplitController.onActivityReparentToTask(TASK_ID, new Intent(), + mActivity.getActivityToken()); + + // Treated as on activity created, but allow to split as primary. + verify(mSplitController).resolveActivityToContainer(mActivity, + true /* canSplitAsPrimary */); + // Try to place the activity to the top TaskFragment when there is no matched rule. + verify(mSplitController).placeActivityInTopContainer(mActivity); + } + + @Test + public void testOnActivityReparentToTask_diffProcess() { + // Create an empty TaskFragment to initialize for the Task. + mSplitController.newContainer(null, mActivity, TASK_ID); + final IBinder activityToken = new Binder(); + final Intent intent = new Intent(); + + mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken); + + // Treated as starting new intent + verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean()); + verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent), + isNull()); + } + + @Test + public void testResolveStartActivityIntent_withoutLaunchingActivity() { + final Intent intent = new Intent(); + final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + + // No other activity available in the Task. + TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNull(container); + + // Task contains another activity that can be used as owner activity. + createMockTaskFragmentContainer(mActivity); + container = mSplitController.resolveStartActivityIntent(mTransaction, + TASK_ID, intent, null /* launchingActivity */); + assertNotNull(container); + } + + @Test + public void testResolveStartActivityIntent_shouldExpand() { + final Intent intent = new Intent(); + setupExpandRule(intent); + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + + assertNotNull(container); + assertTrue(container.areLastRequestedBoundsEqual(null)); + assertTrue(container.isLastRequestedWindowingModeEqual(WINDOWING_MODE_UNDEFINED)); + assertFalse(container.hasActivity(mActivity.getActivityToken())); + verify(mSplitPresenter).createTaskFragment(mTransaction, container.getTaskFragmentToken(), + mActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithLaunchingActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, mActivity); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopExpandActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopSecondaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveStartActivityIntent_shouldSplitWithTopPrimaryActivity() { + final Intent intent = new Intent(); + setupSplitRule(mActivity, intent); + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + + final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( + mTransaction, TASK_ID, intent, null /* launchingActivity */); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + mActivity); + + assertSplitPair(primaryContainer, container); + } + + @Test + public void testPlaceActivityInTopContainer() { + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + + mSplitController.newContainer(null /* activity */, mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter).applyTransaction(any()); + + // Not reparent if activity is in a TaskFragment. + clearInvocations(mSplitPresenter); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.placeActivityInTopContainer(mActivity); + + verify(mSplitPresenter, never()).applyTransaction(any()); + } + + @Test + public void testResolveActivityToContainer_noRuleMatched() { + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + } + + @Test + public void testResolveActivityToContainer_expandRule_notInTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitController).newContainer(mActivity, TASK_ID); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSingleTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken()); + } + + @Test + public void testResolveActivityToContainer_expandRule_inSplitTaskFragment() { + setupExpandRule(mActivity); + + // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it. + final Activity activity = createMockActivity(); + addSplitTaskFragments(activity, mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + assertNotNull(container); + verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is not in any TaskFragment. + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + null /* activityOptions */, placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inOccludedTaskFragment() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is not in the topmost active TaskFragment. + final Activity activity = createMockActivity(); + mSplitController.newContainer(mActivity, TASK_ID); + mSplitController.newContainer(activity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is in the topmost expanded TaskFragment. + mSplitController.newContainer(mActivity, TASK_ID); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + null /* activityOptions */, placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inPrimarySplit() { + setupPlaceholderRule(mActivity); + + // Don't launch placeholder if the activity is in primary split. + final Activity secondaryActivity = createMockActivity(); + addSplitTaskFragments(mActivity, secondaryActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), + anyBoolean()); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() { + setupPlaceholderRule(mActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + + // Launch placeholder if the activity is in secondary split. + final Activity primaryActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT, + null /* activityOptions */, placeholderRule, true /* isPlaceholder */); + } + + @Test + public void testResolveActivityToContainer_splitRule_inPrimarySplitWithRuleMatched() { + final Intent secondaryIntent = new Intent(); + setupSplitRule(mActivity, secondaryIntent); + final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0); + + // Activity is already in primary split, no need to create new split. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer( + null /* activity */, mActivity, TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + splitRule); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + // Activity is already in secondary split, no need to create new split. + addSplitTaskFragments(primaryActivity, mActivity); + clearInvocations(mSplitController); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + verify(mSplitController, never()).newContainer(any(), any(), anyInt()); + verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any()); + } + + @Test + public void testResolveActivityToContainer_splitRule_inSecondarySplitWithNoRuleMatched() { + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, secondaryActivity); + + // Activity is in secondary split, but there is no rule to split it with primary. + addSplitTaskFragments(primaryActivity, secondaryActivity); + mSplitController.getContainerWithActivity(secondaryActivity) + .addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + } + + @Test + public void testResolveActivityToContainer_placeholderRule_isPlaceholderWithRuleMatched() { + final Activity primaryActivity = createMockActivity(); + setupPlaceholderRule(primaryActivity); + final SplitPlaceholderRule placeholderRule = + (SplitPlaceholderRule) mSplitController.getSplitRules().get(0); + doReturn(PLACEHOLDER_INTENT).when(mActivity).getIntent(); + + // Activity is a placeholder. + final TaskFragmentContainer primaryContainer = mSplitController.newContainer( + primaryActivity, TASK_ID); + final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(mActivity, + TASK_ID); + mSplitController.registerSplit( + mTransaction, + primaryContainer, + mActivity, + secondaryContainer, + placeholderRule); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsSecondary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(activityBelow, mActivity); + + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertTrue(result); + assertSplitPair(activityBelow, mActivity); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsPrimary() { + final Activity activityBelow = createMockActivity(); + setupSplitRule(mActivity, activityBelow); + + // Disallow to split as primary. + final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, + TASK_ID); + container.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + assertEquals(container, mSplitController.getContainerWithActivity(mActivity)); + + // Allow to split as primary. + result = mSplitController.resolveActivityToContainer(mActivity, + true /* canSplitAsPrimary */); + + assertTrue(result); + assertSplitPair(mActivity, activityBelow); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsSecondary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(primaryActivity, mActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity( + activityBelow); + secondaryContainer.addPendingAppearedActivity(mActivity); + final boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + final TaskFragmentContainer container = mSplitController.getContainerWithActivity( + mActivity); + + assertTrue(result); + // TODO(b/231845476) we should always respect clearTop. + // assertNotEquals(secondaryContainer, container); + assertSplitPair(primaryContainer, container); + } + + @Test + public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsPrimary() { + final Activity primaryActivity = createMockActivity(); + setupSplitRule(mActivity, primaryActivity); + + final Activity activityBelow = createMockActivity(); + addSplitTaskFragments(primaryActivity, activityBelow); + final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( + primaryActivity); + primaryContainer.addPendingAppearedActivity(mActivity); + boolean result = mSplitController.resolveActivityToContainer(mActivity, + false /* canSplitAsPrimary */); + + assertFalse(result); + assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity)); + + + result = mSplitController.resolveActivityToContainer(mActivity, + true /* canSplitAsPrimary */); + + assertTrue(result); + assertSplitPair(mActivity, primaryActivity); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + doReturn(mActivityResources).when(activity).getResources(); + final IBinder activityToken = new Binder(); + doReturn(activityToken).when(activity).getActivityToken(); + doReturn(activity).when(mSplitController).getActivity(activityToken); + doReturn(TASK_ID).when(activity).getTaskId(); + return activity; + } + + /** Creates a mock TaskFragmentInfo for the given TaskFragment. */ + private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + return new TaskFragmentInfo(container.getTaskFragmentToken(), + mock(WindowContainerToken.class), + new Configuration(), + 1, + true /* isVisible */, + Collections.singletonList(activity.getActivityToken()), + new Point(), + false /* isTaskClearedForReuse */, + false /* isTaskFragmentClearedForPip */); + } + + /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ + private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { + final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID); + setupTaskFragmentInfo(container, activity); + return container; + } + + /** Setups the given TaskFragment as it has appeared in the server. */ + private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container, + @NonNull Activity activity) { + final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity); + container.setInfo(info); + mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info); + } + + /** Setups a rule to always expand the given intent. */ + private void setupExpandRule(@NonNull Intent expandIntent) { + final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** Setups a rule to always expand the given activity. */ + private void setupExpandRule(@NonNull Activity expandActivity) { + final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false) + .setShouldAlwaysExpand(true) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(expandRule)); + } + + /** Setups a rule to launch placeholder for the given activity. */ + private void setupPlaceholderRule(@NonNull Activity primaryActivity) { + final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT, + primaryActivity::equals, i -> false, w -> true) + .setSplitRatio(SPLIT_RATIO) + .build(); + mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); + mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); + } + + /** Setups a rule to always split the given activities. */ + private void setupSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); + mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); + } + + /** Creates a rule to always split the given activity and the given intent. */ + private SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Intent secondaryIntent) { + final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); + return new SplitPairRule.Builder( + activityPair -> false, + targetPair::equals, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setShouldClearTop(true) + .build(); + } + + /** Creates a rule to always split the given activities. */ + private SplitRule createSplitRule(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity); + return new SplitPairRule.Builder( + targetPair::equals, + activityIntentPair -> false, + w -> true) + .setSplitRatio(SPLIT_RATIO) + .setShouldClearTop(true) + .build(); + } + + /** Adds a pair of TaskFragments as split for the given activities. */ + private void addSplitTaskFragments(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer( + primaryActivity); + final TaskFragmentContainer secondaryContainer = createMockTaskFragmentContainer( + secondaryActivity); + mSplitController.registerSplit( + mock(WindowContainerTransaction.class), + primaryContainer, + primaryActivity, + secondaryContainer, + createSplitRule(primaryActivity, secondaryActivity)); + + // We need to set those in case we are not respecting clear top. + // TODO(b/231845476) we should always respect clearTop. + final int windowingMode = mSplitController.getTaskContainer(TASK_ID) + .getWindowingModeForSplitTaskFragment(TASK_BOUNDS); + primaryContainer.setLastRequestedWindowingMode(windowingMode); + secondaryContainer.setLastRequestedWindowingMode(windowingMode); + primaryContainer.setLastRequestedBounds(getSplitBounds(true /* isPrimary */)); + secondaryContainer.setLastRequestedBounds(getSplitBounds(false /* isPrimary */)); + } + + /** Gets the bounds of a TaskFragment that is in split. */ + private Rect getSplitBounds(boolean isPrimary) { + final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO); + return isPrimary + ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width, + TASK_BOUNDS.bottom) + : new Rect(TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right, + TASK_BOUNDS.bottom); + } + + /** Asserts that the two given activities are in split. */ + private void assertSplitPair(@NonNull Activity primaryActivity, + @NonNull Activity secondaryActivity) { + assertSplitPair(mSplitController.getContainerWithActivity(primaryActivity), + mSplitController.getContainerWithActivity(secondaryActivity)); + } + + /** Asserts that the two given TaskFragments are in split. */ + private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer) { + assertNotNull(primaryContainer); + assertNotNull(secondaryContainer); + assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, + secondaryContainer)); + if (primaryContainer.mInfo != null) { + assertTrue(primaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(true /* isPrimary */))); + assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( + WINDOWING_MODE_MULTI_WINDOW)); + } + if (secondaryContainer.mInfo != null) { + assertTrue(secondaryContainer.areLastRequestedBoundsEqual( + getSplitBounds(false /* isPrimary */))); + assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( + WINDOWING_MODE_MULTI_WINDOW)); + } } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index 0de94b0dc26f..f1042ab6ce7d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -24,8 +24,12 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import android.app.Activity; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; @@ -147,4 +151,38 @@ public class TaskContainerTest { assertFalse(taskContainer.isEmpty()); } + + @Test + public void testGetTopTaskFragmentContainer() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); + + final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); + } + + @Test + public void testGetTopNonFinishingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + assertNull(taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf0); + final Activity activity0 = mock(Activity.class); + doReturn(activity0).when(tf0).getTopNonFinishingActivity(); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class); + taskContainer.mContainers.add(tf1); + assertEquals(activity0, taskContainer.getTopNonFinishingActivity()); + + final Activity activity1 = mock(Activity.class); + doReturn(activity1).when(tf1).getTopNonFinishingActivity(); + assertEquals(activity1, taskContainer.getTopNonFinishingActivity()); + } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java index ce80cbf323b2..587878f3bf01 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java @@ -18,6 +18,7 @@ package androidx.window.extensions.embedding; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -29,7 +30,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import android.app.Activity; +import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; @@ -37,6 +40,8 @@ import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.google.android.collect.Lists; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,6 +49,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.List; /** * Test class for {@link TaskFragmentContainer}. @@ -62,16 +68,16 @@ public class TaskFragmentContainerTest { @Mock private SplitController mController; @Mock - private Activity mActivity; - @Mock private TaskFragmentInfo mInfo; @Mock private Handler mHandler; + private Activity mActivity; @Before public void setup() { MockitoAnnotations.initMocks(this); doReturn(mHandler).when(mController).getHandler(); + mActivity = createMockActivity(); } @Test @@ -165,4 +171,72 @@ public class TaskFragmentContainerTest { assertNull(container.mAppearEmptyTimeout); verify(mController).onTaskFragmentAppearEmptyTimeout(container); } + + @Test + public void testCollectActivities() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + List<Activity> activities = container.collectActivities(); + + assertTrue(activities.isEmpty()); + + container.addPendingAppearedActivity(mActivity); + activities = container.collectActivities(); + + assertEquals(1, activities.size()); + + final Activity activity0 = createMockActivity(); + final Activity activity1 = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(), + activity1.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + activities = container.collectActivities(); + + assertEquals(3, activities.size()); + assertEquals(activity0, activities.get(0)); + assertEquals(activity1, activities.get(1)); + assertEquals(mActivity, activities.get(2)); + } + + @Test + public void testAddPendingActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectActivities().size()); + + container.addPendingAppearedActivity(mActivity); + + assertEquals(1, container.collectActivities().size()); + } + + @Test + public void testGetBottomMostActivity() { + final TaskContainer taskContainer = new TaskContainer(TASK_ID); + final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */, + taskContainer, mController); + container.addPendingAppearedActivity(mActivity); + + assertEquals(mActivity, container.getBottomMostActivity()); + + final Activity activity = createMockActivity(); + final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken()); + doReturn(runningActivities).when(mInfo).getActivities(); + container.setInfo(mInfo); + + assertEquals(activity, container.getBottomMostActivity()); + } + + /** Creates a mock activity in the organizer process. */ + private Activity createMockActivity() { + final Activity activity = mock(Activity.class); + final IBinder activityToken = new Binder(); + doReturn(activityToken).when(activity).getActivityToken(); + doReturn(activity).when(mController).getActivity(activityToken); + return activity; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index b04a1fa93bbd..f1ee8fa38485 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -183,7 +183,7 @@ public class BadgedImageView extends ConstraintLayout { getDrawingRect(mTempBounds); - mDrawParams.color = mDotColor; + mDrawParams.dotColor = mDotColor; mDrawParams.iconBounds = mTempBounds; mDrawParams.leftAlign = mOnLeft; mDrawParams.scale = mDotScale; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java index dfd4362e2373..1ea5e21a2c1e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -18,6 +18,7 @@ package com.android.wm.shell.dagger; import android.content.Context; import android.os.Handler; +import android.os.SystemClock; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -39,6 +40,7 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipTransitionState; import com.android.wm.shell.pip.PipUiEventLogger; import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; +import com.android.wm.shell.pip.tv.TvPipBoundsController; import com.android.wm.shell.pip.tv.TvPipBoundsState; import com.android.wm.shell.pip.tv.TvPipController; import com.android.wm.shell.pip.tv.TvPipMenuController; @@ -64,6 +66,7 @@ public abstract class TvPipModule { Context context, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, @@ -74,13 +77,13 @@ public abstract class TvPipModule { PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper windowManagerShellWrapper, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler) { + @ShellMainThread ShellExecutor mainExecutor) { return Optional.of( TvPipController.create( context, tvPipBoundsState, tvPipBoundsAlgorithm, + tvPipBoundsController, pipAppOpsListener, pipTaskOrganizer, pipTransitionController, @@ -91,8 +94,22 @@ public abstract class TvPipModule { pipParamsChangedForwarder, displayController, windowManagerShellWrapper, - mainExecutor, - mainHandler)); + mainExecutor)); + } + + @WMSingleton + @Provides + static TvPipBoundsController provideTvPipBoundsController( + Context context, + @ShellMainThread Handler mainHandler, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm) { + return new TvPipBoundsController( + context, + SystemClock::uptimeMillis, + mainHandler, + tvPipBoundsState, + tvPipBoundsAlgorithm); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 45c9a958a42a..07f2e95dfcb1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1292,7 +1292,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback) { - if (mPipTransitionState.shouldBlockResizeRequest()) { + if (mPipTransitionState.shouldBlockResizeRequest() + || mPipTransitionState.getInSwipePipToHomeTransition()) { return; } if (mWaitForFixedRotation) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java index 21d5d401835d..a2eadcdf6210 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -29,7 +29,6 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Rect; -import android.os.SystemClock; import android.util.ArraySet; import android.util.Size; import android.view.Gravity; @@ -66,7 +65,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { @NonNull PipSnapAlgorithm pipSnapAlgorithm) { super(context, tvPipBoundsState, pipSnapAlgorithm); this.mTvPipBoundsState = tvPipBoundsState; - this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(SystemClock::uptimeMillis); + this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(); reloadResources(context); } @@ -80,7 +79,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { res.getDimensionPixelSize(R.dimen.pip_keep_clear_area_padding)); mKeepClearAlgorithm.setMaxRestrictedDistanceFraction( res.getFraction(R.fraction.config_pipMaxRestrictedMoveDistance, 1, 1)); - mKeepClearAlgorithm.setStashDuration(res.getInteger(R.integer.config_pipStashDuration)); } @Override @@ -104,7 +102,7 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true); } mTvPipBoundsState.setTvPipExpanded(isPipExpanded); - return getTvPipBounds().getBounds(); + return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); } /** Returns the current bounds adjusted to the new aspect ratio, if valid. */ @@ -114,13 +112,27 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio); } - return getTvPipBounds().getBounds(); + return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds()); + } + + Rect adjustBoundsForTemporaryDecor(Rect bounds) { + Rect boundsWithDecor = new Rect(bounds); + Insets decorInset = mTvPipBoundsState.getPipMenuTemporaryDecorInsets(); + Insets pipDecorReverseInsets = Insets.subtract(Insets.NONE, decorInset); + boundsWithDecor.inset(decorInset); + Gravity.apply(mTvPipBoundsState.getTvPipGravity(), + boundsWithDecor.width(), boundsWithDecor.height(), bounds, boundsWithDecor); + + // remove temporary decoration again + boundsWithDecor.inset(pipDecorReverseInsets); + return boundsWithDecor; } /** * Calculates the PiP bounds. */ - public Placement getTvPipBounds() { + @NonNull + public Placement getTvPipPlacement() { final Size pipSize = getPipSize(); final Rect displayBounds = mTvPipBoundsState.getDisplayBounds(); final Size screenSize = new Size(displayBounds.width(), displayBounds.height()); @@ -153,8 +165,6 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset()); mKeepClearAlgorithm.setPipPermanentDecorInsets( mTvPipBoundsState.getPipMenuPermanentDecorInsets()); - mKeepClearAlgorithm.setPipTemporaryDecorInsets( - mTvPipBoundsState.getPipMenuTemporaryDecorInsets()); final Placement placement = mKeepClearAlgorithm.calculatePipPosition( pipSize, @@ -407,8 +417,4 @@ public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { TAG, expandedSize.getWidth(), expandedSize.getHeight()); } } - - void keepUnstashedForCurrentKeepClearAreas() { - mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas(); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java new file mode 100644 index 000000000000..3a6ce81821ec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java @@ -0,0 +1,253 @@ +/* + * 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.wm.shell.pip.tv; + +import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Handler; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Controller managing the PiP's position. + * Manages debouncing of PiP movements and scheduling of unstashing. + */ +public class TvPipBoundsController { + private static final boolean DEBUG = false; + private static final String TAG = "TvPipBoundsController"; + + /** + * Time the calculated PiP position needs to be stable before PiP is moved there, + * to avoid erratic movement. + * Some changes will cause the PiP to be repositioned immediately, such as changes to + * unrestricted keep clear areas. + */ + @VisibleForTesting + static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L; + + private final Context mContext; + private final Supplier<Long> mClock; + private final Handler mMainHandler; + private final TvPipBoundsState mTvPipBoundsState; + private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; + + @Nullable + private PipBoundsListener mListener; + + private int mResizeAnimationDuration; + private int mStashDurationMs; + private Rect mCurrentPlacementBounds; + private Rect mPipTargetBounds; + + private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement; + private boolean mPendingStash; + private Placement mPendingPlacement; + private int mPendingPlacementAnimationDuration; + private Runnable mUnstashRunnable; + + public TvPipBoundsController( + Context context, + Supplier<Long> clock, + Handler mainHandler, + TvPipBoundsState tvPipBoundsState, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm) { + mContext = context; + mClock = clock; + mMainHandler = mainHandler; + mTvPipBoundsState = tvPipBoundsState; + mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; + + loadConfigurations(); + } + + private void loadConfigurations() { + final Resources res = mContext.getResources(); + mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); + mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration); + } + + void setListener(PipBoundsListener listener) { + mListener = listener; + } + + /** + * Update the PiP bounds based on the state of the PiP, decors, and keep clear areas. + * Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid + * keep clear areas, but waits for a new position to stay uncontested for + * {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it. + * Temporary decor changes are applied immediately. + * + * @param stayAtAnchorPosition If true, PiP will be placed at the anchor position + * @param disallowStashing If true, PiP will not be placed off-screen in a stashed position + * @param animationDuration Duration of the animation to the new position + * @param immediate If true, PiP will move immediately to avoid keep clear areas + */ + @VisibleForTesting + void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing, + int animationDuration, boolean immediate) { + final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement(); + + final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType(); + mTvPipBoundsState.setStashed(stashType); + if (stayAtAnchorPosition) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getAnchorBounds(), animationDuration); + } else if (disallowStashing) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getUnstashedBounds(), animationDuration); + } else if (immediate) { + cancelScheduledPlacement(); + applyPlacementBounds(placement.getBounds(), animationDuration); + scheduleUnstashIfNeeded(placement); + } else { + applyPlacementBounds(mCurrentPlacementBounds, animationDuration); + schedulePinnedStackPlacement(placement, animationDuration); + } + } + + private void schedulePinnedStackPlacement(@NonNull final Placement placement, + int animationDuration) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: schedulePinnedStackPlacement() - pip bounds: %s", + TAG, placement.getBounds().toShortString()); + } + + if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(), + placement.getBounds())) { + mPendingStash = mPendingStash || placement.getTriggerStash(); + return; + } + + mPendingStash = placement.getStashType() != STASH_TYPE_NONE + && (mPendingStash || placement.getTriggerStash()); + + mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); + mPendingPlacement = placement; + mPendingPlacementAnimationDuration = animationDuration; + mMainHandler.postAtTime(mApplyPendingPlacementRunnable, + mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS); + } + + private void scheduleUnstashIfNeeded(final Placement placement) { + if (mUnstashRunnable != null) { + mMainHandler.removeCallbacks(mUnstashRunnable); + mUnstashRunnable = null; + } + if (placement.getUnstashDestinationBounds() != null) { + mUnstashRunnable = () -> { + applyPlacementBounds(placement.getUnstashDestinationBounds(), + mResizeAnimationDuration); + mUnstashRunnable = null; + }; + mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs); + } + } + + private void applyPendingPlacement() { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: applyPendingPlacement()", TAG); + } + if (mPendingPlacement != null) { + if (mPendingStash) { + mPendingStash = false; + scheduleUnstashIfNeeded(mPendingPlacement); + } + + if (mUnstashRunnable != null) { + // currently stashed, use stashed pos + applyPlacementBounds(mPendingPlacement.getBounds(), + mPendingPlacementAnimationDuration); + } else { + applyPlacementBounds(mPendingPlacement.getUnstashedBounds(), + mPendingPlacementAnimationDuration); + } + } + + mPendingPlacement = null; + } + + void onPipDismissed() { + mCurrentPlacementBounds = null; + mPipTargetBounds = null; + cancelScheduledPlacement(); + } + + private void cancelScheduledPlacement() { + mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable); + mPendingPlacement = null; + + if (mUnstashRunnable != null) { + mMainHandler.removeCallbacks(mUnstashRunnable); + mUnstashRunnable = null; + } + } + + private void applyPlacementBounds(Rect bounds, int animationDuration) { + if (bounds == null) { + return; + } + + mCurrentPlacementBounds = bounds; + Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds); + movePipTo(adjustedBounds, animationDuration); + } + + /** Animates the PiP to the given bounds with the given animation duration. */ + private void movePipTo(Rect bounds, int animationDuration) { + if (Objects.equals(mPipTargetBounds, bounds)) { + return; + } + + mPipTargetBounds = bounds; + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString()); + } + + if (mListener != null) { + mListener.onPipTargetBoundsChange(bounds, animationDuration); + } + } + + /** + * Interface being notified of changes to the PiP bounds as calculated by + * @link TvPipBoundsController}. + */ + public interface PipBoundsListener { + /** + * Called when the calculated PiP bounds are changing. + * + * @param newTargetBounds The new bounds of the PiP. + * @param animationDuration The animation duration for the PiP movement. + */ + void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 766779413094..fa48def9c7d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -25,12 +25,10 @@ import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.app.RemoteAction; import android.app.TaskInfo; -import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; -import android.os.Handler; import android.os.RemoteException; import android.view.Gravity; @@ -46,25 +44,24 @@ import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipAppOpsListener; -import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; -import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; +import java.util.Objects; import java.util.Set; /** * Manages the picture-in-picture (PIP) UI and states. */ public class TvPipController implements PipTransitionController.PipTransitionCallback, - TvPipMenuController.Delegate, TvPipNotificationController.Delegate, - DisplayController.OnDisplaysChangedListener { + TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate, + TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener { private static final String TAG = "TvPipController"; static final boolean DEBUG = false; @@ -98,19 +95,18 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final TvPipBoundsState mTvPipBoundsState; private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; + private final TvPipBoundsController mTvPipBoundsController; private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; private final TvPipNotificationController mPipNotificationController; private final TvPipMenuController mTvPipMenuController; private final ShellExecutor mMainExecutor; - private final Handler mMainHandler; private final TvPipImpl mImpl = new TvPipImpl(); private @State int mState = STATE_NO_PIP; private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY; private int mPinnedTaskId = NONEXISTENT_TASK_ID; - private Runnable mUnstashRunnable; private RemoteAction mCloseAction; // How long the shell will wait for the app to close the PiP if a custom action is set. @@ -123,6 +119,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal Context context, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, @@ -133,12 +130,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShell, - ShellExecutor mainExecutor, - Handler mainHandler) { + ShellExecutor mainExecutor) { return new TvPipController( context, tvPipBoundsState, tvPipBoundsAlgorithm, + tvPipBoundsController, pipAppOpsListener, pipTaskOrganizer, pipTransitionController, @@ -149,14 +146,14 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipParamsChangedForwarder, displayController, wmShell, - mainExecutor, - mainHandler).mImpl; + mainExecutor).mImpl; } private TvPipController( Context context, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm, + TvPipBoundsController tvPipBoundsController, PipAppOpsListener pipAppOpsListener, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, @@ -167,16 +164,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShell, - ShellExecutor mainExecutor, - Handler mainHandler) { + ShellExecutor mainExecutor) { mContext = context; mMainExecutor = mainExecutor; - mMainHandler = mainHandler; mTvPipBoundsState = tvPipBoundsState; mTvPipBoundsState.setDisplayId(context.getDisplayId()); mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; + mTvPipBoundsController = tvPipBoundsController; + mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; @@ -227,7 +224,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal /** * Starts the process if bringing up the Pip menu if by issuing a command to move Pip * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip - * task/window is properly positioned in {@link #onPipTransitionFinished(ComponentName, int)}. + * task/window is properly positioned in {@link #onPipTransitionFinished(int)}. */ @Override public void showPictureInPictureMenu() { @@ -256,7 +253,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: closeMenu(), state before=%s", TAG, stateToName(mState)); } setState(STATE_PIP); - mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas(); updatePinnedStackBounds(); } @@ -331,68 +327,35 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) { if (mTvPipBoundsState.getDisplayId() == displayId) { + boolean unrestrictedAreasChanged = !Objects.equals(unrestricted, + mTvPipBoundsState.getUnrestrictedKeepClearAreas()); mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted); - updatePinnedStackBounds(); + updatePinnedStackBounds(mResizeAnimationDuration, unrestrictedAreasChanged); } } private void updatePinnedStackBounds() { - updatePinnedStackBounds(mResizeAnimationDuration); + updatePinnedStackBounds(mResizeAnimationDuration, true); } /** * Update the PiP bounds based on the state of the PiP and keep clear areas. - * Animates to the current PiP bounds, and schedules unstashing the PiP if necessary. */ - private void updatePinnedStackBounds(int animationDuration) { + private void updatePinnedStackBounds(int animationDuration, boolean immediate) { if (mState == STATE_NO_PIP) { return; } - final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode(); final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition; - final Placement placement = mTvPipBoundsAlgorithm.getTvPipBounds(); - - int stashType = - disallowStashing ? PipBoundsState.STASH_TYPE_NONE : placement.getStashType(); - mTvPipBoundsState.setStashed(stashType); - - if (stayAtAnchorPosition) { - movePinnedStackTo(placement.getAnchorBounds()); - } else if (disallowStashing) { - movePinnedStackTo(placement.getUnstashedBounds()); - } else { - movePinnedStackTo(placement.getBounds()); - } - - if (mUnstashRunnable != null) { - mMainHandler.removeCallbacks(mUnstashRunnable); - mUnstashRunnable = null; - } - if (!disallowStashing && placement.getUnstashDestinationBounds() != null) { - mUnstashRunnable = () -> { - movePinnedStackTo(placement.getUnstashDestinationBounds(), animationDuration); - }; - mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime()); - } - } - - /** Animates the PiP to the given bounds. */ - private void movePinnedStackTo(Rect bounds) { - movePinnedStackTo(bounds, mResizeAnimationDuration); + mTvPipBoundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing, + animationDuration, immediate); } - /** Animates the PiP to the given bounds with the given animation duration. */ - private void movePinnedStackTo(Rect bounds, int animationDuration) { - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString()); - } - mPipTaskOrganizer.scheduleAnimateResizePip(bounds, - animationDuration, rect -> { - mTvPipMenuController.updateExpansionState(); - }); - mTvPipMenuController.onPipTransitionStarted(bounds); + @Override + public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) { + mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds, + animationDuration, rect -> mTvPipMenuController.updateExpansionState()); + mTvPipMenuController.onPipTransitionStarted(newTargetBounds); } /** @@ -431,7 +394,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void closeEduText() { - updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs); + updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); } private void registerSessionListenerForCurrentUser() { @@ -473,6 +436,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPipNotificationController.dismiss(); mTvPipMenuController.closeMenu(); mTvPipBoundsState.resetTvPipState(); + mTvPipBoundsController.onPipDismissed(); setState(STATE_NO_PIP); mPinnedTaskId = NONEXISTENT_TASK_ID; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt index 07dccd58abfd..1e54436ebce9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt @@ -33,17 +33,14 @@ import kotlin.math.min import kotlin.math.roundToInt private const val DEFAULT_PIP_MARGINS = 48 -private const val DEFAULT_STASH_DURATION = 5000L private const val RELAX_DEPTH = 1 private const val DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION = 0.15 /** * This class calculates an appropriate position for a Picture-In-Picture (PiP) window, taking * into account app defined keep clear areas. - * - * @param clock A function returning a current timestamp (in milliseconds) */ -class TvPipKeepClearAlgorithm(private val clock: () -> Long) { +class TvPipKeepClearAlgorithm() { /** * Result of the positioning algorithm. * @@ -51,17 +48,17 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { * @param anchorBounds The bounds of the PiP anchor position * (where the PiP would be placed if there were no keep clear areas) * @param stashType Where the PiP has been stashed, if at all - * @param unstashDestinationBounds If stashed, the PiP should move to this position after - * [stashDuration] has passed. - * @param unstashTime If stashed, the time at which the PiP should move - * to [unstashDestinationBounds] + * @param unstashDestinationBounds If stashed, the PiP should move to this position when + * unstashing. + * @param triggerStash Whether this placement should trigger the PiP to stash, or extend + * the unstash timeout if already stashed. */ data class Placement( val bounds: Rect, val anchorBounds: Rect, @PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE, val unstashDestinationBounds: Rect? = null, - val unstashTime: Long = 0L + val triggerStash: Boolean = false ) { /** Bounds to use if the PiP should not be stashed. */ fun getUnstashedBounds() = unstashDestinationBounds ?: bounds @@ -79,12 +76,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { /** The distance the PiP peeks into the screen when stashed */ var stashOffset = DEFAULT_PIP_MARGINS - /** - * How long (in milliseconds) the PiP should stay stashed for after the last time the - * keep clear areas causing the PiP to stash have changed. - */ - var stashDuration = DEFAULT_STASH_DURATION - /** The fraction of screen width/height restricted keep clear areas can move the PiP */ var maxRestrictedDistanceFraction = DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION @@ -93,14 +84,10 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { private var transformedMovementBounds = Rect() private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet() - private var lastStashTime: Long = Long.MIN_VALUE /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP * decorations are relevant for calculating intersecting keep clear areas */ private var pipPermanentDecorInsets = Insets.NONE - /** Spaces around the PiP that we should leave space for when placing the PiP. Temporary PiP - * decorations are not relevant for calculating intersecting keep clear areas */ - private var pipTemporaryDecorInsets = Insets.NONE /** * Calculates the position the PiP should be placed at, taking into consideration the @@ -113,8 +100,8 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { * always try to respect these areas. * * If no free space the PiP is allowed to move to can be found, a stashed position is returned - * as [Placement.bounds], along with a position to move to once [Placement.unstashTime] has - * passed as [Placement.unstashDestinationBounds]. + * as [Placement.bounds], along with a position to move to when the PiP unstashes + * as [Placement.unstashDestinationBounds]. * * @param pipSize The size of the PiP window * @param restrictedAreas The restricted keep clear areas @@ -130,13 +117,11 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas) val pipSizeWithAllDecors = addDecors(pipSize) - val pipAnchorBoundsWithAllDecors = + val pipAnchorBoundsWithDecors = getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds) - val pipAnchorBoundsWithPermanentDecors = - removeTemporaryDecorsTransformed(pipAnchorBoundsWithAllDecors) val result = calculatePipPositionTransformed( - pipAnchorBoundsWithPermanentDecors, + pipAnchorBoundsWithDecors, transformedRestrictedAreas, transformedUnrestrictedAreas ) @@ -152,7 +137,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { anchorBounds, getStashType(pipBounds, unstashedDestBounds), unstashedDestBounds, - result.unstashTime + result.triggerStash ) } @@ -213,26 +198,13 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { !lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition) lastAreasOverlappingUnstashPosition = areasOverlappingUnstashPosition - val now = clock() - if (areasOverlappingUnstashPositionChanged) { - lastStashTime = now - } - - // If overlapping areas haven't changed and the stash duration has passed, we can - // place the PiP at the unstash position - val unstashTime = lastStashTime + stashDuration - if (now >= unstashTime) { - return Placement(unstashBounds, pipAnchorBounds) - } - - // Otherwise, we'll stash it close to the unstash position val stashedBounds = getNearbyStashedPosition(unstashBounds, keepClearAreas) return Placement( stashedBounds, pipAnchorBounds, getStashType(stashedBounds, unstashBounds), unstashBounds, - unstashTime + areasOverlappingUnstashPositionChanged ) } @@ -439,14 +411,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { } /** - * Prevents the PiP from being stashed for the current set of keep clear areas. - * The PiP may stash again if keep clear areas change. - */ - fun keepUnstashedForCurrentKeepClearAreas() { - lastStashTime = Long.MIN_VALUE - } - - /** * Updates the size of the screen. * * @param size The new size of the screen @@ -492,10 +456,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { pipPermanentDecorInsets = insets } - fun setPipTemporaryDecorInsets(insets: Insets) { - pipTemporaryDecorInsets = insets - } - /** * @param open Whether this event marks the opening of an occupied segment * @param pos The coordinate of this event @@ -790,7 +750,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { private fun addDecors(size: Size): Size { val bounds = Rect(0, 0, size.width, size.height) bounds.inset(pipPermanentDecorInsets) - bounds.inset(pipTemporaryDecorInsets) return Size(bounds.width(), bounds.height()) } @@ -805,19 +764,6 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { return bounds } - /** - * Removes the space that was reserved for temporary decorations around the PiP - * @param bounds the bounds (in base case) to remove the insets from - */ - private fun removeTemporaryDecorsTransformed(bounds: Rect): Rect { - if (pipTemporaryDecorInsets == Insets.NONE) return bounds - - val reverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets) - val boundsInScreenSpace = fromTransformedSpace(bounds) - boundsInScreenSpace.inset(reverseInsets) - return toTransformedSpace(boundsInScreenSpace) - } - private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) } private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 132c04481bce..4ce45e142c64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -209,7 +209,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMovementMenuOnly()", TAG); } - mInMoveMode = true; + setInMoveMode(true); mCloseAfterExitMoveMenu = true; showMenuInternal(); } @@ -219,7 +219,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG); } - mInMoveMode = false; + setInMoveMode(false); mCloseAfterExitMoveMenu = false; showMenuInternal(); } @@ -293,6 +293,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis return mInMoveMode; } + private void setInMoveMode(boolean moveMode) { + if (mInMoveMode == moveMode) { + return; + } + + mInMoveMode = moveMode; + if (mDelegate != null) { + mDelegate.onInMoveModeChanged(); + } + } + @Override public void onEnterMoveMode() { if (DEBUG) { @@ -300,7 +311,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, mCloseAfterExitMoveMenu); } - mInMoveMode = true; + setInMoveMode(true); mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); } @@ -312,13 +323,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mCloseAfterExitMoveMenu); } if (mCloseAfterExitMoveMenu) { - mInMoveMode = false; + setInMoveMode(false); mCloseAfterExitMoveMenu = false; closeMenu(); return true; } if (mInMoveMode) { - mInMoveMode = false; + setInMoveMode(false); mPipMenuView.showButtonsMenu(); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 63774fb0d8ca..1d976cea96a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -1115,7 +1115,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); mDividerFadeInAnimator = ValueAnimator.ofFloat(0f, 1f); mDividerFadeInAnimator.addUpdateListener(animation -> { - if (dividerLeash == null) { + if (dividerLeash == null || !dividerLeash.isValid()) { mDividerFadeInAnimator.cancel(); return; } @@ -1125,7 +1125,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDividerFadeInAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - if (dividerLeash == null) { + if (dividerLeash == null || !dividerLeash.isValid()) { mDividerFadeInAnimator.cancel(); return; } diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index a899709c1405..ea10be564351 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -37,6 +37,7 @@ android_test { "androidx.test.ext.junit", "androidx.dynamicanimation_dynamicanimation", "dagger2", + "frameworks-base-testutils", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "mockito-target-extended-minus-junit4", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt new file mode 100644 index 000000000000..05e472245b4a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt @@ -0,0 +1,255 @@ +/* + * 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.wm.shell.pip.tv + +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.os.Handler +import android.os.test.TestLooper +import android.testing.AndroidTestingRunner + +import com.android.wm.shell.R +import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT +import com.android.wm.shell.pip.tv.TvPipBoundsController.POSITION_DEBOUNCE_TIMEOUT_MILLIS +import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.AdditionalAnswers.returnsFirstArg +import org.mockito.Mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +class TvPipBoundsControllerTest { + val ANIMATION_DURATION = 100 + val STASH_DURATION = 5000 + val FAR_FUTURE = 60 * 60000L + val ANCHOR_BOUNDS = Rect(90, 90, 100, 100) + val STASHED_BOUNDS = Rect(99, 90, 109, 100) + val MOVED_BOUNDS = Rect(90, 80, 100, 90) + val STASHED_MOVED_BOUNDS = Rect(99, 80, 109, 90) + val ANCHOR_PLACEMENT = Placement(ANCHOR_BOUNDS, ANCHOR_BOUNDS) + val STASHED_PLACEMENT = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, ANCHOR_BOUNDS, false) + val STASHED_PLACEMENT_RESTASH = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, ANCHOR_BOUNDS, true) + val MOVED_PLACEMENT = Placement(MOVED_BOUNDS, ANCHOR_BOUNDS) + val STASHED_MOVED_PLACEMENT = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, MOVED_BOUNDS, false) + val STASHED_MOVED_PLACEMENT_RESTASH = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS, + STASH_TYPE_RIGHT, MOVED_BOUNDS, true) + + lateinit var boundsController: TvPipBoundsController + var time = 0L + lateinit var testLooper: TestLooper + lateinit var mainHandler: Handler + + var inMenu = false + var inMoveMode = false + + @Mock + lateinit var context: Context + @Mock + lateinit var resources: Resources + @Mock + lateinit var tvPipBoundsState: TvPipBoundsState + @Mock + lateinit var tvPipBoundsAlgorithm: TvPipBoundsAlgorithm + @Mock + lateinit var listener: TvPipBoundsController.PipBoundsListener + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + time = 0L + inMenu = false + inMoveMode = false + + testLooper = TestLooper { time } + mainHandler = Handler(testLooper.getLooper()) + + whenever(context.resources).thenReturn(resources) + whenever(resources.getInteger(R.integer.config_pipStashDuration)).thenReturn(STASH_DURATION) + whenever(tvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(any())) + .then(returnsFirstArg<Rect>()) + + boundsController = TvPipBoundsController( + context, + { time }, + mainHandler, + tvPipBoundsState, + tvPipBoundsAlgorithm) + boundsController.setListener(listener) + } + + @Test + fun testPlacement_MovedAfterDebounceTimeout() { + triggerPlacement(MOVED_PLACEMENT) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testStashedPlacement_MovedAfterDebounceTimeout_Unstashes() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testDebounceSamePlacement_MovesDebounceTimeoutAfterFirstPlacement() { + triggerPlacement(MOVED_PLACEMENT) + advanceTimeTo(POSITION_DEBOUNCE_TIMEOUT_MILLIS / 2) + triggerPlacement(MOVED_PLACEMENT) + + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + } + + @Test + fun testNoMovementUntilPlacementStabilizes() { + triggerPlacement(ANCHOR_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(MOVED_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(ANCHOR_PLACEMENT) + advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10) + triggerPlacement(MOVED_PLACEMENT) + + assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS) + } + + @Test + fun testUnstashIfStashNoLongerNecessary() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + triggerPlacement(ANCHOR_PLACEMENT) + assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, ANCHOR_BOUNDS) + } + + @Test + fun testRestashingPlacementDelaysUnstash() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + assertNoMovementUpTo(time + STASH_DURATION / 2) + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertNoMovementUpTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testNonRestashingPlacementDoesNotDelayUnstash() { + triggerPlacement(STASHED_PLACEMENT_RESTASH) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS) + + assertNoMovementUpTo(time + STASH_DURATION / 2) + triggerPlacement(STASHED_PLACEMENT) + assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testImmediatePlacement() { + triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH) + assertMovement(STASHED_BOUNDS) + assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS) + } + + @Test + fun testInMoveMode_KeepAtAnchor() { + startMoveMode() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(ANCHOR_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testInMenu_Unstashed() { + openPipMenu() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(MOVED_BOUNDS) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + @Test + fun testCloseMenu_DoNotRestash() { + openPipMenu() + triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH) + assertMovement(MOVED_BOUNDS) + + closePipMenu() + triggerPlacement(STASHED_MOVED_PLACEMENT) + assertNoMovementUpTo(time + FAR_FUTURE) + } + + fun assertMovement(bounds: Rect) { + verify(listener).onPipTargetBoundsChange(eq(bounds), anyInt()) + reset(listener) + } + + fun assertMovementAt(timeMs: Long, bounds: Rect) { + assertNoMovementUpTo(timeMs - 1) + advanceTimeTo(timeMs) + assertMovement(bounds) + } + + fun assertNoMovementUpTo(timeMs: Long) { + advanceTimeTo(timeMs) + verify(listener, never()).onPipTargetBoundsChange(any(), anyInt()) + } + + fun triggerPlacement(placement: Placement, immediate: Boolean = false) { + whenever(tvPipBoundsAlgorithm.getTvPipPlacement()).thenReturn(placement) + val stayAtAnchorPosition = inMoveMode + val disallowStashing = inMenu || stayAtAnchorPosition + boundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing, + ANIMATION_DURATION, immediate) + } + + fun triggerImmediatePlacement(placement: Placement) { + triggerPlacement(placement, true) + } + + fun openPipMenu() { + inMenu = true + inMoveMode = false + } + + fun closePipMenu() { + inMenu = false + inMoveMode = false + } + + fun startMoveMode() { + inMenu = true + inMoveMode = true + } + + fun advanceTimeTo(ms: Long) { + time = ms + testLooper.dispatchAll() + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt index 46f388d0ce0e..0fcc5cf384c9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt @@ -30,7 +30,9 @@ import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement import org.junit.Before import org.junit.Test import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse import junit.framework.Assert.assertNull +import junit.framework.Assert.assertTrue @RunWith(AndroidTestingRunner::class) class TvPipKeepClearAlgorithmTest { @@ -46,7 +48,6 @@ class TvPipKeepClearAlgorithmTest { private lateinit var pipSize: Size private lateinit var movementBounds: Rect private lateinit var algorithm: TvPipKeepClearAlgorithm - private var currentTime = 0L private var restrictedAreas = mutableSetOf<Rect>() private var unrestrictedAreas = mutableSetOf<Rect>() private var gravity: Int = 0 @@ -58,16 +59,14 @@ class TvPipKeepClearAlgorithmTest { restrictedAreas.clear() unrestrictedAreas.clear() - currentTime = 0L pipSize = DEFAULT_PIP_SIZE gravity = Gravity.BOTTOM or Gravity.RIGHT - algorithm = TvPipKeepClearAlgorithm({ currentTime }) + algorithm = TvPipKeepClearAlgorithm() algorithm.setScreenSize(SCREEN_SIZE) algorithm.setMovementBounds(movementBounds) algorithm.pipAreaPadding = PADDING algorithm.stashOffset = STASH_OFFSET - algorithm.stashDuration = 5000L algorithm.setGravity(gravity) algorithm.maxRestrictedDistanceFraction = 0.3 } @@ -265,7 +264,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_BOTTOM, placement.stashType) assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) + assertTrue(placement.triggerStash) } @Test @@ -305,7 +304,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) + assertTrue(placement.triggerStash) } @Test @@ -352,9 +351,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) - - currentTime += 1000 + assertTrue(placement.triggerStash) restrictedAreas.remove(sideBar) placement = getActualPlacement() @@ -363,7 +360,7 @@ class TvPipKeepClearAlgorithmTest { } @Test - fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() { + fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() { gravity = Gravity.BOTTOM or Gravity.RIGHT val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT) @@ -384,13 +381,13 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) - - currentTime += algorithm.stashDuration + assertTrue(placement.triggerStash) placement = getActualPlacement() - assertEquals(expectedUnstashBounds, placement.bounds) - assertNotStashed(placement) + assertEquals(expectedBounds, placement.bounds) + assertEquals(STASH_TYPE_RIGHT, placement.stashType) + assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) + assertFalse(placement.triggerStash) } @Test @@ -415,9 +412,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(algorithm.stashDuration, placement.unstashTime) - - currentTime += 1000 + assertTrue(placement.triggerStash) val newObstruction = Rect( 0, @@ -431,7 +426,7 @@ class TvPipKeepClearAlgorithmTest { assertEquals(expectedBounds, placement.bounds) assertEquals(STASH_TYPE_RIGHT, placement.stashType) assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds) - assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime) + assertTrue(placement.triggerStash) } @Test @@ -458,21 +453,9 @@ class TvPipKeepClearAlgorithmTest { @Test fun test_PipInsets() { - val permInsets = Insets.of(-1, -2, -3, -4) - algorithm.setPipPermanentDecorInsets(permInsets) - testInsetsForAllPositions(permInsets) - - val tempInsets = Insets.of(-4, -3, -2, -1) - algorithm.setPipPermanentDecorInsets(Insets.NONE) - algorithm.setPipTemporaryDecorInsets(tempInsets) - testInsetsForAllPositions(tempInsets) - - algorithm.setPipPermanentDecorInsets(permInsets) - algorithm.setPipTemporaryDecorInsets(tempInsets) - testInsetsForAllPositions(Insets.add(permInsets, tempInsets)) - } + val insets = Insets.of(-1, -2, -3, -4) + algorithm.setPipPermanentDecorInsets(insets) - private fun testInsetsForAllPositions(insets: Insets) { gravity = Gravity.BOTTOM or Gravity.RIGHT testAnchorPositionWithInsets(insets) @@ -546,6 +529,6 @@ class TvPipKeepClearAlgorithmTest { private fun assertNotStashed(actual: Placement) { assertEquals(STASH_TYPE_NONE, actual.stashType) assertNull(actual.unstashDestinationBounds) - assertEquals(0L, actual.unstashTime) + assertFalse(actual.triggerStash) } } diff --git a/libs/hwui/AnimatorManager.cpp b/libs/hwui/AnimatorManager.cpp index 4826d5a0c8da..078041411a21 100644 --- a/libs/hwui/AnimatorManager.cpp +++ b/libs/hwui/AnimatorManager.cpp @@ -31,7 +31,8 @@ static void detach(sp<BaseRenderNodeAnimator>& animator) { animator->detach(); } -AnimatorManager::AnimatorManager(RenderNode& parent) : mParent(parent), mAnimationHandle(nullptr) {} +AnimatorManager::AnimatorManager(RenderNode& parent) + : mParent(parent), mAnimationHandle(nullptr), mCancelAllAnimators(false) {} AnimatorManager::~AnimatorManager() { for_each(mNewAnimators.begin(), mNewAnimators.end(), detach); @@ -82,8 +83,16 @@ void AnimatorManager::pushStaging() { } mNewAnimators.clear(); } - for (auto& animator : mAnimators) { - animator->pushStaging(mAnimationHandle->context()); + + if (mCancelAllAnimators) { + for (auto& animator : mAnimators) { + animator->forceEndNow(mAnimationHandle->context()); + } + mCancelAllAnimators = false; + } else { + for (auto& animator : mAnimators) { + animator->pushStaging(mAnimationHandle->context()); + } } } @@ -184,5 +193,9 @@ void AnimatorManager::endAllActiveAnimators() { mAnimationHandle->release(); } +void AnimatorManager::forceEndAnimators() { + mCancelAllAnimators = true; +} + } /* namespace uirenderer */ } /* namespace android */ diff --git a/libs/hwui/AnimatorManager.h b/libs/hwui/AnimatorManager.h index a0df01d5962c..6002661dc82a 100644 --- a/libs/hwui/AnimatorManager.h +++ b/libs/hwui/AnimatorManager.h @@ -16,11 +16,11 @@ #ifndef ANIMATORMANAGER_H #define ANIMATORMANAGER_H -#include <vector> - #include <cutils/compiler.h> #include <utils/StrongPointer.h> +#include <vector> + #include "utils/Macros.h" namespace android { @@ -56,6 +56,8 @@ public: // Hard-ends all animators. May only be called on the UI thread. void endAllStagingAnimators(); + void forceEndAnimators(); + // Hard-ends all animators that have been pushed. Used for cleanup if // the ActivityContext is being destroyed void endAllActiveAnimators(); @@ -71,6 +73,8 @@ private: // To improve the efficiency of resizing & removing from the vector std::vector<sp<BaseRenderNodeAnimator> > mNewAnimators; std::vector<sp<BaseRenderNodeAnimator> > mAnimators; + + bool mCancelAllAnimators; }; } /* namespace uirenderer */ diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index 944393c63ad6..db7639029187 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -543,6 +543,12 @@ static void android_view_RenderNode_endAllAnimators(JNIEnv* env, jobject clazz, renderNode->animators().endAllStagingAnimators(); } +static void android_view_RenderNode_forceEndAnimators(JNIEnv* env, jobject clazz, + jlong renderNodePtr) { + RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr); + renderNode->animators().forceEndAnimators(); +} + // ---------------------------------------------------------------------------- // SurfaceView position callback // ---------------------------------------------------------------------------- @@ -745,6 +751,7 @@ static const JNINativeMethod gMethods[] = { {"nGetAllocatedSize", "(J)I", (void*)android_view_RenderNode_getAllocatedSize}, {"nAddAnimator", "(JJ)V", (void*)android_view_RenderNode_addAnimator}, {"nEndAllAnimators", "(J)V", (void*)android_view_RenderNode_endAllAnimators}, + {"nForceEndAnimators", "(J)V", (void*)android_view_RenderNode_forceEndAnimators}, {"nRequestPositionUpdates", "(JLjava/lang/ref/WeakReference;)V", (void*)android_view_RenderNode_requestPositionUpdates}, diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java index 11cacd01f53d..fe58cca9395f 100644 --- a/media/java/android/media/AudioDeviceVolumeManager.java +++ b/media/java/android/media/AudioDeviceVolumeManager.java @@ -235,13 +235,7 @@ public class AudioDeviceVolumeManager { mDeviceVolumeDispatcherStub = new DeviceVolumeDispatcherStub(); } } else { - for (ListenerInfo info : mDeviceVolumeListeners) { - if (info.mListener == vclistener) { - throw new IllegalArgumentException( - "attempt to call setDeviceAbsoluteMultiVolumeBehavior() " - + "on a previously registered listener"); - } - } + mDeviceVolumeListeners.removeIf(info -> info.mDevice.equalTypeAddress(device)); } mDeviceVolumeListeners.add(listenerInfo); mDeviceVolumeDispatcherStub.register(true, device, volumes, handlesVolumeAdjustment); @@ -304,6 +298,28 @@ public class AudioDeviceVolumeManager { "removeOnDeviceVolumeBehaviorChangedListener"); } + /** + * Return human-readable name for volume behavior + * @param behavior one of the volume behaviors defined in AudioManager + * @return a string for the given behavior + */ + public static String volumeBehaviorName(@AudioManager.DeviceVolumeBehavior int behavior) { + switch (behavior) { + case AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE: + return "DEVICE_VOLUME_BEHAVIOR_VARIABLE"; + case AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL: + return "DEVICE_VOLUME_BEHAVIOR_FULL"; + case AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED: + return "DEVICE_VOLUME_BEHAVIOR_FIXED"; + case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE: + return "DEVICE_VOLUME_BEHAVIOR_ABSOLUTE"; + case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE: + return "DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE"; + default: + return "invalid volume behavior " + behavior; + } + } + private final class DeviceVolumeBehaviorDispatcherStub extends IDeviceVolumeBehaviorDispatcher.Stub implements CallbackUtil.DispatcherStub { public void register(boolean register) { diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 55c558f36880..85cd342b5e11 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -2910,6 +2910,10 @@ public class AudioTrack extends PlayerBase * For portability, an application should prime the data path to the maximum allowed * by writing data until the write() method returns a short transfer count. * This allows play() to start immediately, and reduces the chance of underrun. + *<p> + * As of {@link android.os.Build.VERSION_CODES#S} the minimum level to start playing + * can be obtained using {@link #getStartThresholdInFrames()} and set with + * {@link #setStartThresholdInFrames(int)}. * * @throws IllegalStateException if the track isn't properly initialized */ diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl index fed86dc9e0a8..2e792b394301 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl @@ -22,10 +22,10 @@ import android.media.tv.interactive.TvInteractiveAppServiceInfo; * Interface to receive callbacks from ITvInteractiveAppManager regardless of sessions. * @hide */ -interface ITvInteractiveAppManagerCallback { +oneway interface ITvInteractiveAppManagerCallback { void onInteractiveAppServiceAdded(in String iAppServiceId); void onInteractiveAppServiceRemoved(in String iAppServiceId); void onInteractiveAppServiceUpdated(in String iAppServiceId); void onTvInteractiveAppServiceInfoUpdated(in TvInteractiveAppServiceInfo tvIAppInfo); void onStateChanged(in String iAppServiceId, int type, int state, int err); -}
\ No newline at end of file +} diff --git a/media/jni/android_media_Utils.cpp b/media/jni/android_media_Utils.cpp index f4a39b3469bb..b7ad6dcf9354 100644 --- a/media/jni/android_media_Utils.cpp +++ b/media/jni/android_media_Utils.cpp @@ -18,10 +18,10 @@ #define LOG_TAG "AndroidMediaUtils" #include <aidl/android/hardware/graphics/common/PlaneLayoutComponentType.h> -#include <hardware/camera3.h> #include <ui/GraphicBufferMapper.h> #include <ui/GraphicTypes.h> #include <utils/Log.h> + #include "android_media_Utils.h" #define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) ) @@ -122,8 +122,8 @@ uint32_t Image_getBlobSize(LockedImage* buffer, bool usingRGBAOverride) { } // First check for BLOB transport header at the end of the buffer - uint8_t* header = blobBuffer + (width - sizeof(struct camera3_jpeg_blob)); - struct camera3_jpeg_blob *blob = (struct camera3_jpeg_blob*)(header); + uint8_t* header = blobBuffer + (width - sizeof(struct camera3_jpeg_blob_v2)); + struct camera3_jpeg_blob_v2 *blob = (struct camera3_jpeg_blob_v2*)(header); if (blob->jpeg_blob_id == CAMERA3_JPEG_BLOB_ID || blob->jpeg_blob_id == CAMERA3_HEIC_BLOB_ID) { size = blob->jpeg_size; diff --git a/media/jni/android_media_Utils.h b/media/jni/android_media_Utils.h index 4feb4f516f1e..c12cec129ede 100644 --- a/media/jni/android_media_Utils.h +++ b/media/jni/android_media_Utils.h @@ -50,6 +50,20 @@ int getBufferWidth(BufferItem *buffer); int getBufferHeight(BufferItem *buffer); +// Must be in sync with AIDL CameraBlob : android.hardware.camera.device.CameraBlob +// HALs must NOT copy this definition. +// for details: http://b/229688810 +typedef struct camera3_jpeg_blob_v2 { + uint32_t jpeg_blob_id; + uint32_t jpeg_size; +} camera3_jpeg_blobv2_t; + +// Must be in sync with AIDL CameraBlob : android.hardware.camera.device.CameraBlobId +enum { + CAMERA3_JPEG_BLOB_ID = 0x00FF, + CAMERA3_JPEG_APP_SEGMENTS_BLOB_ID = 0x0100, +}; + }; // namespace android #endif // _ANDROID_MEDIA_UTILS_H_ diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java index f333b86d4d52..fc5ff085139c 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java @@ -123,6 +123,7 @@ public class CompanionDeviceDiscoveryService extends Service { intent.setAction(ACTION_START_DISCOVERY); intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest); sStateLiveData.setValue(DiscoveryState.STARTING); + sScanResultsLiveData.setValue(Collections.emptyList()); context.startService(intent); } @@ -173,7 +174,6 @@ public class CompanionDeviceDiscoveryService extends Service { @Override public void onDestroy() { - sScanResultsLiveData.setValue(Collections.emptyList()); super.onDestroy(); if (DEBUG) Log.d(TAG, "onDestroy()"); } @@ -187,7 +187,6 @@ public class CompanionDeviceDiscoveryService extends Service { mStopAfterFirstMatch = request.isSingleDevice(); mDiscoveryStarted = true; sStateLiveData.setValue(DiscoveryState.DISCOVERY_IN_PROGRESS); - sScanResultsLiveData.setValue(Collections.emptyList()); final List<DeviceFilter<?>> allFilters = request.getDeviceFilters(); final List<BluetoothDeviceFilter> btFilters = diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java index faca1ae3f058..1f59d30207d8 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java @@ -47,7 +47,7 @@ class DeviceFilterPair<T extends Parcelable> { } String getDisplayName() { - if (mFilter != null) mFilter.getDeviceDisplayName(mDevice); + if (mFilter != null) return mFilter.getDeviceDisplayName(mDevice); if (mDevice instanceof BluetoothDevice) { return getDeviceDisplayNameInternal((BluetoothDevice) mDevice); diff --git a/packages/CtsShim/apk/arm/CtsShim.apk b/packages/CtsShim/apk/arm/CtsShim.apk Binary files differindex 0c3c4bb24208..fb092862b79e 100644 --- a/packages/CtsShim/apk/arm/CtsShim.apk +++ b/packages/CtsShim/apk/arm/CtsShim.apk diff --git a/packages/CtsShim/apk/arm/CtsShimPriv.apk b/packages/CtsShim/apk/arm/CtsShimPriv.apk Binary files differindex ee42d081f2f2..07915ce76bb5 100644 --- a/packages/CtsShim/apk/arm/CtsShimPriv.apk +++ b/packages/CtsShim/apk/arm/CtsShimPriv.apk diff --git a/packages/CtsShim/apk/x86/CtsShim.apk b/packages/CtsShim/apk/x86/CtsShim.apk Binary files differindex 0c3c4bb24208..fb092862b79e 100644 --- a/packages/CtsShim/apk/x86/CtsShim.apk +++ b/packages/CtsShim/apk/x86/CtsShim.apk diff --git a/packages/CtsShim/apk/x86/CtsShimPriv.apk b/packages/CtsShim/apk/x86/CtsShimPriv.apk Binary files differindex 2bb3750b200d..20e94b6f2dac 100644 --- a/packages/CtsShim/apk/x86/CtsShimPriv.apk +++ b/packages/CtsShim/apk/x86/CtsShimPriv.apk diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp index 7cf5385bd841..af3e2102e430 100644 --- a/packages/CtsShim/build/Android.bp +++ b/packages/CtsShim/build/Android.bp @@ -47,6 +47,7 @@ android_app { uses_libs: ["android.test.runner"], apex_available: [ + "//apex_available:platform", "com.android.apex.cts.shim.v2_apk_in_apex_upgrades", ], } diff --git a/packages/SettingsLib/AndroidManifest.xml b/packages/SettingsLib/AndroidManifest.xml index 3e67abfe1960..13f8a372c9b5 100644 --- a/packages/SettingsLib/AndroidManifest.xml +++ b/packages/SettingsLib/AndroidManifest.xml @@ -22,16 +22,6 @@ <activity android:name="com.android.settingslib.users.AvatarPickerActivity" android:theme="@style/SudThemeGlifV2.DayNight"/> - - <activity - android:name="com.android.settingslib.qrcode.QrCodeScanModeActivity" - android:exported="true"> - <intent-filter> - <action android:name="android.settings.BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER"/> - <category android:name="android.intent.category.DEFAULT"/> - </intent-filter> - </activity> - </application> </manifest> diff --git a/packages/SettingsLib/AppPreference/res/layout/preference_app_header.xml b/packages/SettingsLib/AppPreference/res/layout/preference_app_header.xml new file mode 100644 index 000000000000..e0d01f9bb2c3 --- /dev/null +++ b/packages/SettingsLib/AppPreference/res/layout/preference_app_header.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingBottom="16dp" + android:paddingTop="8dp" + android:clickable="false"> + + <TextView + android:id="@+id/apps_top_intro_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textDirection="locale" + android:clickable="false" + android:longClickable="false" + android:textAppearance="@style/TextAppearance.TopIntroText" /> + +</LinearLayout> diff --git a/packages/SettingsLib/SettingsTheme/res/values/styles.xml b/packages/SettingsLib/SettingsTheme/res/values/styles.xml index aaab0f041fe3..00bd14122251 100644 --- a/packages/SettingsLib/SettingsTheme/res/values/styles.xml +++ b/packages/SettingsLib/SettingsTheme/res/values/styles.xml @@ -26,4 +26,11 @@ <style name="TextAppearance.CategoryTitle.SettingsLib" parent="@android:style/TextAppearance.DeviceDefault.Medium"> </style> + + <style name="TextAppearance.TopIntroText" + parent="@android:style/TextAppearance.DeviceDefault"> + <item name="android:textSize">14sp</item> + <item name="android:textColor">?android:attr/textColorSecondary</item> + </style> + </resources> diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp index cd0bdea9425f..ecf2a72021ed 100644 --- a/packages/SettingsLib/TopIntroPreference/Android.bp +++ b/packages/SettingsLib/TopIntroPreference/Android.bp @@ -16,6 +16,7 @@ android_library { static_libs: [ "androidx.annotation_annotation", "androidx.preference_preference", + "SettingsLibSettingsTheme", ], sdk_version: "system_current", min_sdk_version: "21", diff --git a/packages/SettingsLib/TopIntroPreference/res/values/styles.xml b/packages/SettingsLib/TopIntroPreference/res/values/styles.xml deleted file mode 100644 index b6ca41fb6b6d..000000000000 --- a/packages/SettingsLib/TopIntroPreference/res/values/styles.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2021 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. - --> -<resources> - <style name="TextAppearance.TopIntroText" - parent="@android:style/TextAppearance.DeviceDefault"> - <item name="android:textSize">14sp</item> - <item name="android:textColor">?android:attr/textColorSecondary</item> - </style> -</resources> diff --git a/packages/SettingsLib/res/drawable/ic_qr_code_scanner.xml b/packages/SettingsLib/res/drawable/ic_qr_code_scanner.xml deleted file mode 100644 index f6f63c5ae7fa..000000000000 --- a/packages/SettingsLib/res/drawable/ic_qr_code_scanner.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - 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. ---> - -<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" - android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> - <path android:fillColor="@android:color/white" - android:pathData="M2,7V2H7V4H4V7ZM2,22V17H4V20H7V22ZM17,22V20H20V17H22V22ZM20,7V4H17V2H22V7ZM17.5,17.5H19V19H17.5ZM17.5,14.5H19V16H17.5ZM16,16H17.5V17.5H16ZM14.5,17.5H16V19H14.5ZM13,16H14.5V17.5H13ZM16,13H17.5V14.5H16ZM14.5,14.5H16V16H14.5ZM13,13H14.5V14.5H13ZM19,5V11H13V5ZM11,13V19H5V13ZM11,5V11H5V5ZM9.5,17.5V14.5H6.5V17.5ZM9.5,9.5V6.5H6.5V9.5ZM17.5,9.5V6.5H14.5V9.5Z"/> -</vector>
\ No newline at end of file diff --git a/packages/SettingsLib/res/layout/qrcode_scan_mode_activity.xml b/packages/SettingsLib/res/layout/qrcode_scan_mode_activity.xml deleted file mode 100644 index f0a182b3d67b..000000000000 --- a/packages/SettingsLib/res/layout/qrcode_scan_mode_activity.xml +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - 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. ---> - -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/root" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/fragment_container" - android:layout_width="match_parent" - android:layout_height="match_parent"/> - -</LinearLayout> diff --git a/packages/SettingsLib/res/layout/qrcode_scanner_fragment.xml b/packages/SettingsLib/res/layout/qrcode_scanner_fragment.xml deleted file mode 100644 index 0a7fe0903348..000000000000 --- a/packages/SettingsLib/res/layout/qrcode_scanner_fragment.xml +++ /dev/null @@ -1,102 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - 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. ---> - -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/sud_layout_icon_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="3" - android:layout_marginBottom="35dp"> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="bottom" - android:gravity="center" - android:orientation="vertical"> - <ImageView - android:id="@+id/sud_layout_icon" - android:src="@drawable/ic_qr_code_scanner" - android:tint="?androidprv:attr/colorAccentPrimaryVariant" - android:layout_width="@dimen/qrcode_icon_size" - android:layout_height="@dimen/qrcode_icon_size" - android:contentDescription="@null"/> - - <TextView - android:id="@+id/sud_layout_title" - style="@style/QrCodeScanner" - android:textSize="24sp" - android:text="@string/bt_le_audio_scan_qr_code" - android:gravity="center" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="19dp"/> - - <TextView - android:id="@+id/sud_layout_subtitle" - style="@style/QrCodeScanner" - android:text="@string/bt_le_audio_scan_qr_code_scanner" - android:gravity="center" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="8dp"/> - </LinearLayout> - </LinearLayout> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_weight="7" - android:orientation="vertical"> - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="top" - android:gravity="center" - android:clipChildren="true"> - <TextureView - android:id="@+id/preview_view" - android:layout_marginStart="@dimen/qrcode_preview_margin" - android:layout_marginEnd="@dimen/qrcode_preview_margin" - android:layout_width="match_parent" - android:layout_height="@dimen/qrcode_preview_size"/> - </FrameLayout> - - <TextView - android:id="@+id/error_message" - style="@style/TextAppearance.ErrorText" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:layout_marginStart="?attr/sudMarginStart" - android:layout_marginEnd="?attr/sudMarginEnd" - android:gravity="center" - android:layout_gravity="center" - android:visibility="invisible"/> - - </LinearLayout> - - -</LinearLayout> - diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml index cbc79d2d5d93..226b119ebb76 100644 --- a/packages/SettingsLib/res/values/dimens.xml +++ b/packages/SettingsLib/res/values/dimens.xml @@ -115,12 +115,6 @@ <!-- Minimum density scale. This is available on all devices. --> <fraction name="display_density_min_scale">85%</fraction> - <!-- QR code picture size --> - <dimen name="qrcode_preview_size">360dp</dimen> - <dimen name="qrcode_preview_margin">40dp</dimen> - <dimen name="qrcode_preview_radius">30dp</dimen> - <dimen name="qrcode_icon_size">27dp</dimen> - <!-- Broadcast dialog --> <dimen name="broadcast_dialog_title_img_margin_top">18dp</dimen> <dimen name="broadcast_dialog_title_text_size">24sp</dimen> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index a171f86dd217..847f1dc541e8 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1589,13 +1589,6 @@ <!-- Description for a setting which controls whether an app can turn the screen on [CHAR LIMIT=NONE] --> <string name="allow_turn_screen_on_description">Allow an app to turn the screen on. If granted, the app may turn on the screen at any time without your explicit intent.</string> - <!-- [CHAR LIMIT=NONE] Le audio QR code scanner title --> - <string name="bt_le_audio_scan_qr_code">Scan QR code</string> - <!-- [CHAR LIMIT=NONE] Le audio QR code scanner sub-title --> - <string name="bt_le_audio_scan_qr_code_scanner">To start listening, center the QR code below</string> - <!-- [CHAR LIMIT=NONE] Hint for QR code process failure --> - <string name="bt_le_audio_qr_code_is_not_valid_format">QR code isn\u0027t a valid format</string> - <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, title --> <string name="bt_le_audio_broadcast_dialog_title">Stop broadcasting <xliff:g id="app_name" example="App Name 1">%1$s</xliff:g>?</string> <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, sub-title --> diff --git a/packages/SettingsLib/res/values/styles.xml b/packages/SettingsLib/res/values/styles.xml index 32345150531d..5237b4fa1c3d 100644 --- a/packages/SettingsLib/res/values/styles.xml +++ b/packages/SettingsLib/res/values/styles.xml @@ -33,13 +33,6 @@ <item name="android:textColor">?android:attr/colorError</item> </style> - <style name="QrCodeScanner"> - <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> - <item name="android:textSize">16sp</item> - <item name="android:textColor">?android:attr/textColorPrimary</item> - <item name="android:textDirection">locale</item> - </style> - <style name="BroadcastDialogTitleStyle"> <item name="android:textAppearance">@style/TextAppearanceBroadcastDialogTitle</item> <item name="android:layout_marginStart">@dimen/broadcast_dialog_title_text_margin</item> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index c9af4d53f9a8..fea7475fc087 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -30,6 +30,9 @@ import com.android.settingslib.widget.AdaptiveOutlineDrawable; import java.io.IOException; import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class BluetoothUtils { private static final String TAG = "BluetoothUtils"; @@ -39,6 +42,8 @@ public class BluetoothUtils { public static final int META_INT_ERROR = -1; public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled"; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH"; private static ErrorListener sErrorListener; @@ -384,8 +389,43 @@ public class BluetoothUtils { return Uri.parse(data); } + /** + * Get URI Bluetooth metadata for extra control + * + * @param bluetoothDevice the BluetoothDevice to get metadata + * @return the URI metadata + */ + public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) { + String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS); + return extraTagValue(KEY_HEARABLE_CONTROL_SLICE, data); + } + @SuppressLint("NewApi") // Hidden API made public private static boolean doesClassMatch(BluetoothClass btClass, int classId) { return btClass.doesClassMatch(classId); } + + private static String extraTagValue(String tag, String metaData) { + if (TextUtils.isEmpty(metaData)) { + return null; + } + Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)")); + Matcher matcher = pattern.matcher(metaData); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + private static String getTagStart(String tag) { + return String.format(Locale.ENGLISH, "<%s>", tag); + } + + private static String getTagEnd(String tag) { + return String.format(Locale.ENGLISH, "</%s>", tag); + } + + private static String generateExpressionWithTag(String tag, String value) { + return getTagStart(tag) + value + getTagEnd(tag); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java index dd7db21f765c..afafd9f27e95 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java @@ -59,7 +59,7 @@ public class BluetoothMediaDevice extends MediaDevice { @Override public Drawable getIcon() { - return BluetoothUtils.getBtDrawableWithDescription(mContext, mCachedDevice).first; + return BluetoothUtils.getBtClassDrawableWithDescription(mContext, mCachedDevice).first; } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java deleted file mode 100644 index 15a910e13aa8..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.settingslib.qrcode; - -import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_DEVICE_SINK; -import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP; - -import android.bluetooth.BluetoothDevice; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import androidx.fragment.app.FragmentTransaction; - -import com.android.settingslib.R; -import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; -import com.android.settingslib.bluetooth.BluetoothUtils; - -public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity { - private static final boolean DEBUG = BluetoothUtils.D; - private static final String TAG = "QrCodeScanModeActivity"; - - private boolean mIsGroupOp; - private BluetoothDevice mSink; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - protected void handleIntent(Intent intent) { - String action = intent != null ? intent.getAction() : null; - if (DEBUG) { - Log.d(TAG, "handleIntent(), action = " + action); - } - - if (action == null) { - finish(); - return; - } - - switch (action) { - case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER: - showQrCodeScannerFragment(intent); - break; - default: - if (DEBUG) { - Log.e(TAG, "Launch with an invalid action"); - } - finish(); - } - } - - protected void showQrCodeScannerFragment(Intent intent) { - if (DEBUG) { - Log.d(TAG, "showQrCodeScannerFragment"); - } - - if (intent != null) { - mSink = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE_SINK); - mIsGroupOp = intent.getBooleanExtra(EXTRA_BLUETOOTH_SINK_IS_GROUP, false); - if (DEBUG) { - Log.d(TAG, "get extra from intent"); - } - } else { - if (DEBUG) { - Log.d(TAG, "intent is null, can not get bluetooth information from intent."); - } - } - - QrCodeScanModeFragment fragment = - (QrCodeScanModeFragment) mFragmentManager.findFragmentByTag( - BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER); - - if (fragment == null) { - fragment = new QrCodeScanModeFragment(mIsGroupOp, mSink); - } else { - if (fragment.isVisible()) { - return; - } - - // When the fragment in back stack but not on top of the stack, we can simply pop - // stack because current fragment transactions are arranged in an order - mFragmentManager.popBackStackImmediate(); - return; - } - final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction(); - - fragmentTransaction.replace(R.id.fragment_container, fragment, - BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER); - fragmentTransaction.commit(); - } -} - diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java deleted file mode 100644 index 361fd5b57556..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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.settingslib.qrcode; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.fragment.app.FragmentManager; - -import com.android.settingslib.R; -import com.android.settingslib.core.lifecycle.ObservableActivity; - -public abstract class QrCodeScanModeBaseActivity extends ObservableActivity { - - protected FragmentManager mFragmentManager; - - protected abstract void handleIntent(Intent intent); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setTheme(R.style.SudThemeGlifV3_DayNight); - - setContentView(R.layout.qrcode_scan_mode_activity); - mFragmentManager = getSupportFragmentManager(); - - if (savedInstanceState == null) { - handleIntent(getIntent()); - } - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java deleted file mode 100644 index 153d2d20e801..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 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.settingslib.qrcode; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothLeBroadcastMetadata; -import android.content.Context; -import android.util.Log; - -import com.android.settingslib.bluetooth.BluetoothUtils; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; -import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; - -public class QrCodeScanModeController { - - private static final boolean DEBUG = BluetoothUtils.D; - private static final String TAG = "QrCodeScanModeController"; - - private LocalBluetoothLeBroadcastMetadata mLocalBroadcastMetadata; - private LocalBluetoothLeBroadcastAssistant mLocalBroadcastAssistant; - private LocalBluetoothManager mLocalBluetoothManager; - private LocalBluetoothProfileManager mProfileManager; - - private LocalBluetoothManager.BluetoothManagerCallback - mOnInitCallback = new LocalBluetoothManager.BluetoothManagerCallback() { - @Override - public void onBluetoothManagerInitialized(Context appContext, - LocalBluetoothManager bluetoothManager) { - BluetoothUtils.setErrorListener(mErrorListener); - } - }; - - private BluetoothUtils.ErrorListener - mErrorListener = new BluetoothUtils.ErrorListener() { - @Override - public void onShowError(Context context, String name, int messageResId) { - if (DEBUG) { - Log.d(TAG, "Get error when initializing BluetoothManager. "); - } - } - }; - - public QrCodeScanModeController(Context context) { - if (DEBUG) { - Log.d(TAG, "QrCodeScanModeController constructor."); - } - mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, mOnInitCallback); - mProfileManager = mLocalBluetoothManager.getProfileManager(); - mLocalBroadcastMetadata = new LocalBluetoothLeBroadcastMetadata(); - CachedBluetoothDeviceManager cachedDeviceManager = new CachedBluetoothDeviceManager(context, - mLocalBluetoothManager); - mLocalBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(context, - cachedDeviceManager, mProfileManager); - } - - private BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) { - return mLocalBroadcastMetadata.convertToBroadcastMetadata(qrCodeString); - } - - public void addSource(BluetoothDevice sink, String sourceMetadata, - boolean isGroupOp) { - mLocalBroadcastAssistant.addSource(sink, - convertToBroadcastMetadata(sourceMetadata), isGroupOp); - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java deleted file mode 100644 index 069b9507ccef..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java +++ /dev/null @@ -1,235 +0,0 @@ -/** - * 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.settingslib.qrcode; - -import android.bluetooth.BluetoothDevice; -import android.content.Context; -import android.graphics.Matrix; -import android.graphics.Outline; -import android.graphics.Rect; -import android.graphics.SurfaceTexture; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import android.util.Size; -import android.view.LayoutInflater; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.accessibility.AccessibilityEvent; -import android.widget.TextView; - -import com.android.settingslib.R; -import com.android.settingslib.bluetooth.BluetoothBroadcastUtils; -import com.android.settingslib.bluetooth.BluetoothUtils; -import com.android.settingslib.core.lifecycle.ObservableFragment; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; - -public class QrCodeScanModeFragment extends ObservableFragment implements - TextureView.SurfaceTextureListener, - QrCamera.ScannerCallback { - private static final boolean DEBUG = BluetoothUtils.D; - private static final String TAG = "QrCodeScanModeFragment"; - - /** Message sent to hide error message */ - private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1; - /** Message sent to show error message */ - private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2; - /** Message sent to broadcast QR code */ - private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3; - - private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000; - private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000; - - private boolean mIsGroupOp; - private int mCornerRadius; - private BluetoothDevice mSink; - private String mBroadcastMetadata; - private Context mContext; - private QrCamera mCamera; - private QrCodeScanModeController mController; - private TextureView mTextureView; - private TextView mSummary; - private TextView mErrorMessage; - - public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) { - mIsGroupOp = isGroupOp; - mSink = sink; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mContext = getContext(); - mController = new QrCodeScanModeController(mContext); - } - - @Override - public final View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.qrcode_scanner_fragment, container, - /* attachToRoot */ false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - mTextureView = view.findViewById(R.id.preview_view); - mCornerRadius = mContext.getResources().getDimensionPixelSize( - R.dimen.qrcode_preview_radius); - mTextureView.setSurfaceTextureListener(this); - mTextureView.setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - outline.setRoundRect(0,0, view.getWidth(), view.getHeight(), mCornerRadius); - } - }); - mTextureView.setClipToOutline(true); - mErrorMessage = view.findViewById(R.id.error_message); - } - - private void initCamera(SurfaceTexture surface) { - // Check if the camera has already created. - if (mCamera == null) { - mCamera = new QrCamera(mContext, this); - mCamera.start(surface); - } - } - - private void destroyCamera() { - if (mCamera != null) { - mCamera.stop(); - mCamera = null; - } - } - - @Override - public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) { - initCamera(surface); - } - - @Override - public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, - int height) { - } - - @Override - public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) { - destroyCamera(); - return true; - } - - @Override - public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) { - } - - @Override - public void handleSuccessfulResult(String qrCode) { - if (DEBUG) { - Log.d(TAG, "handleSuccessfulResult(), get the qr code string."); - } - mBroadcastMetadata = qrCode; - handleBtLeAudioScanner(); - } - - @Override - public void handleCameraFailure() { - destroyCamera(); - } - - @Override - public Size getViewSize() { - return new Size(mTextureView.getWidth(), mTextureView.getHeight()); - } - - @Override - public Rect getFramePosition(Size previewSize, int cameraOrientation) { - return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight()); - } - - @Override - public void setTransform(Matrix transform) { - mTextureView.setTransform(transform); - } - - @Override - public boolean isValid(String qrCode) { - if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) { - return true; - } else { - showErrorMessage(R.string.bt_le_audio_qr_code_is_not_valid_format); - return false; - } - } - - protected boolean isDecodeTaskAlive() { - return mCamera != null && mCamera.isDecodeTaskAlive(); - } - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_HIDE_ERROR_MESSAGE: - mErrorMessage.setVisibility(View.INVISIBLE); - break; - - case MESSAGE_SHOW_ERROR_MESSAGE: - final String errorMessage = (String) msg.obj; - - mErrorMessage.setVisibility(View.VISIBLE); - mErrorMessage.setText(errorMessage); - mErrorMessage.sendAccessibilityEvent( - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - - // Cancel any pending messages to hide error view and requeue the message so - // user has time to see error - removeMessages(MESSAGE_HIDE_ERROR_MESSAGE); - sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE, - SHOW_ERROR_MESSAGE_INTERVAL); - break; - - case MESSAGE_SCAN_BROADCAST_SUCCESS: - mController.addSource(mSink, mBroadcastMetadata, mIsGroupOp); - updateSummary(); - mSummary.sendAccessibilityEvent( - AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - break; - default: - } - } - }; - - private void showErrorMessage(@StringRes int messageResId) { - final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, - getString(messageResId)); - message.sendToTarget(); - } - - private void handleBtLeAudioScanner() { - Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS); - mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL); - } - - private void updateSummary() { - mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner, - null /* broadcast_name*/));; - } -} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 2e855145e152..1c0ea1a1f4b0 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -51,6 +51,11 @@ public class BluetoothUtilsTest { private static final String STRING_METADATA = "string_metadata"; private static final String BOOL_METADATA = "true"; private static final String INT_METADATA = "25"; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; + private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH"; + private static final String CONTROL_METADATA = + "<HEARABLE_CONTROL_SLICE_WITH_WIDTH>" + STRING_METADATA + + "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>"; @Before public void setUp() { @@ -152,6 +157,15 @@ public class BluetoothUtilsTest { } @Test + public void getControlUriMetaData_hasMetaData_returnsCorrectMetaData() { + when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)).thenReturn( + CONTROL_METADATA.getBytes()); + + assertThat(BluetoothUtils.getControlUriMetaData(mBluetoothDevice)).isEqualTo( + STRING_METADATA); + } + + @Test public void isAdvancedDetailsHeader_untetheredHeadset_returnTrue() { when(mBluetoothDevice.getMetadata( BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn( diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index 0c6d40aa9910..5088533ccdd8 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -1074,13 +1074,63 @@ public class SettingsBackupAgent extends BackupAgentHelper { if (DEBUG) Log.d(TAG, "Successfully unMarshaled SoftApConfiguration "); // Depending on device hardware, we may need to notify the user of a setting change SoftApConfiguration storedConfig = mWifiManager.getSoftApConfiguration(); - if (!storedConfig.equals(configInCloud)) { + + if (isNeedToNotifyUserConfigurationHasChanged(configInCloud, storedConfig)) { Log.d(TAG, "restored ap configuration requires a conversion, notify the user"); WifiSoftApConfigChangedNotifier.notifyUserOfConfigConversion(this); } } } + private boolean isNeedToNotifyUserConfigurationHasChanged(SoftApConfiguration configInCloud, + SoftApConfiguration storedConfig) { + // Check if the cloud configuration was modified when restored to the device. + // All elements of the configuration are compared except: + // 1. Persistent randomized MAC address (which is per device) + // 2. The flag indicating whether the configuration is "user modified" + return !(Objects.equals(configInCloud.getWifiSsid(), storedConfig.getWifiSsid()) + && Objects.equals(configInCloud.getBssid(), storedConfig.getBssid()) + && Objects.equals(configInCloud.getPassphrase(), storedConfig.getPassphrase()) + && configInCloud.isHiddenSsid() == storedConfig.isHiddenSsid() + && configInCloud.getChannels().toString().equals( + storedConfig.getChannels().toString()) + && configInCloud.getSecurityType() == storedConfig.getSecurityType() + && configInCloud.getMaxNumberOfClients() == storedConfig.getMaxNumberOfClients() + && configInCloud.isAutoShutdownEnabled() == storedConfig.isAutoShutdownEnabled() + && configInCloud.getShutdownTimeoutMillis() + == storedConfig.getShutdownTimeoutMillis() + && configInCloud.isClientControlByUserEnabled() + == storedConfig.isClientControlByUserEnabled() + && Objects.equals(configInCloud.getBlockedClientList(), + storedConfig.getBlockedClientList()) + && Objects.equals(configInCloud.getAllowedClientList(), + storedConfig.getAllowedClientList()) + && configInCloud.getMacRandomizationSetting() + == storedConfig.getMacRandomizationSetting() + && configInCloud.isBridgedModeOpportunisticShutdownEnabled() + == storedConfig.isBridgedModeOpportunisticShutdownEnabled() + && configInCloud.isIeee80211axEnabled() == storedConfig.isIeee80211axEnabled() + && configInCloud.isIeee80211beEnabled() == storedConfig.isIeee80211beEnabled() + && configInCloud.getBridgedModeOpportunisticShutdownTimeoutMillis() + == storedConfig.getBridgedModeOpportunisticShutdownTimeoutMillis() + && Objects.equals(configInCloud.getVendorElements(), + storedConfig.getVendorElements()) + && (configInCloud.getPersistentRandomizedMacAddress() != null + ? Objects.equals(configInCloud.getPersistentRandomizedMacAddress(), + storedConfig.getPersistentRandomizedMacAddress()) : true) + && Arrays.equals(configInCloud.getAllowedAcsChannels( + SoftApConfiguration.BAND_2GHZ), + storedConfig.getAllowedAcsChannels(SoftApConfiguration.BAND_2GHZ)) + && Arrays.equals(configInCloud.getAllowedAcsChannels( + SoftApConfiguration.BAND_5GHZ), + storedConfig.getAllowedAcsChannels(SoftApConfiguration.BAND_5GHZ)) + && Arrays.equals(configInCloud.getAllowedAcsChannels( + SoftApConfiguration.BAND_6GHZ), + storedConfig.getAllowedAcsChannels(SoftApConfiguration.BAND_6GHZ)) + && configInCloud.getMaxChannelBandwidth() == storedConfig.getMaxChannelBandwidth() + ); + } + private byte[] getNetworkPolicies() { NetworkPolicyManager networkPolicyManager = (NetworkPolicyManager) getSystemService(NETWORK_POLICY_SERVICE); diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 290ce345694e..4d0888ab8d2d 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -323,6 +323,8 @@ <!-- To read safety center status --> <uses-permission android:name="android.permission.READ_SAFETY_CENTER_STATUS" /> + <uses-permission android:name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" /> + <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index dd45b6f39bdd..0c8202260907 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -21,7 +21,6 @@ import android.app.WallpaperColors import android.graphics.Color import com.android.internal.graphics.ColorUtils import com.android.internal.graphics.cam.Cam -import com.android.internal.graphics.cam.CamUtils.lstarFromInt import kotlin.math.absoluteValue import kotlin.math.max import kotlin.math.roundToInt @@ -50,7 +49,7 @@ internal interface Hue { val previousHue = hueAndRotations[previousIndex].first if (ColorScheme.angleIsBetween(sourceHue, thisHue, previousHue)) { return ColorScheme.wrapDegreesDouble(sourceHue.toDouble() + - hueAndRotations[previousIndex].first) + hueAndRotations[previousIndex].second) } } @@ -143,12 +142,24 @@ internal class ChromaMinimum(val chroma: Double) : Chroma { } } +internal class ChromaMultiple(val multiple: Double) : Chroma { + override fun get(sourceColor: Cam): Double { + return sourceColor.chroma * multiple + } +} + internal class ChromaConstant(val chroma: Double) : Chroma { override fun get(sourceColor: Cam): Double { return chroma } } +internal class ChromaSource : Chroma { + override fun get(sourceColor: Cam): Double { + return sourceColor.chroma.toDouble() + } +} + internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) { fun shades(sourceColor: Cam): List<Int> { val hue = hue.get(sourceColor) @@ -184,8 +195,8 @@ enum class Style(internal val coreSpec: CoreSpec) { a1 = TonalSpec(HueSource(), ChromaMinimum(48.0)), a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)), a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)), - n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), - n2 = TonalSpec(HueSource(), ChromaConstant(12.0)) + n1 = TonalSpec(HueSource(), ChromaConstant(12.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(14.0)) )), EXPRESSIVE(CoreSpec( a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)), @@ -208,6 +219,13 @@ enum class Style(internal val coreSpec: CoreSpec) { n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), n2 = TonalSpec(HueSource(), ChromaConstant(16.0)) )), + CONTENT(CoreSpec( + a1 = TonalSpec(HueSource(), ChromaSource()), + a2 = TonalSpec(HueSource(), ChromaMultiple(0.33)), + a3 = TonalSpec(HueSource(), ChromaMultiple(0.66)), + n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), + n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) + )), } class ColorScheme( @@ -231,7 +249,7 @@ class ColorScheme( darkTheme: Boolean, style: Style = Style.TONAL_SPOT ): - this(getSeedColor(wallpaperColors), darkTheme, style) + this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allAccentColors: List<Int> get() { @@ -260,7 +278,7 @@ class ColorScheme( val proposedSeedCam = Cam.fromInt(seed) val seedArgb = if (seed == Color.TRANSPARENT) { GOOGLE_BLUE - } else if (proposedSeedCam.chroma < 5) { + } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { GOOGLE_BLUE } else { seed @@ -289,22 +307,26 @@ class ColorScheme( * Identifies a color to create a color scheme from. * * @param wallpaperColors Colors extracted from an image via quantization. + * @param filter If false, allow colors that have low chroma, creating grayscale themes. * @return ARGB int representing the color */ @JvmStatic + @JvmOverloads @ColorInt - fun getSeedColor(wallpaperColors: WallpaperColors): Int { - return getSeedColors(wallpaperColors).first() + fun getSeedColor(wallpaperColors: WallpaperColors, filter: Boolean = true): Int { + return getSeedColors(wallpaperColors, filter).first() } /** * Filters and ranks colors from WallpaperColors. * * @param wallpaperColors Colors extracted from an image via quantization. + * @param filter If false, allow colors that have low chroma, creating grayscale themes. * @return List of ARGB ints, ordered from highest scoring to lowest. */ @JvmStatic - fun getSeedColors(wallpaperColors: WallpaperColors): List<Int> { + @JvmOverloads + fun getSeedColors(wallpaperColors: WallpaperColors, filter: Boolean = true): List<Int> { val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b } .toDouble() val totalPopulationMeaningless = (totalPopulation == 0.0) @@ -317,9 +339,12 @@ class ColorScheme( val distinctColors = wallpaperColors.mainColors.map { it.toArgb() }.distinct().filter { - Cam.fromInt(it).chroma >= MIN_CHROMA + if (!filter) { + true + } else { + Cam.fromInt(it).chroma >= MIN_CHROMA + } }.toList() - if (distinctColors.isEmpty()) { return listOf(GOOGLE_BLUE) } @@ -332,7 +357,7 @@ class ColorScheme( val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) } // Get an array with 360 slots. A slot contains the percentage of colors with that hue. - val hueProportions = huePopulations(intToCam, intToProportion) + val hueProportions = huePopulations(intToCam, intToProportion, filter) // Map each color to the percentage of the image with its hue. val intToHueProportion = wallpaperColors.allColors.mapValues { val cam = intToCam[it.key]!! @@ -346,13 +371,12 @@ class ColorScheme( // Remove any inappropriate seed colors. For example, low chroma colors look grayscale // raising their chroma will turn them to a much louder color that may not have been // in the image. - val filteredIntToCam = intToCam.filter { + val filteredIntToCam = if (!filter) intToCam else (intToCam.filter { val cam = it.value - val lstar = lstarFromInt(it.key) val proportion = intToHueProportion[it.key]!! cam.chroma >= MIN_CHROMA && (totalPopulationMeaningless || proportion > 0.01) - } + }) // Sort the colors by score, from high to low. val intToScoreIntermediate = filteredIntToCam.mapValues { score(it.value, intToHueProportion[it.key]!!) @@ -444,7 +468,8 @@ class ColorScheme( private fun huePopulations( camByColor: Map<Int, Cam>, - populationByColor: Map<Int, Double> + populationByColor: Map<Int, Double>, + filter: Boolean = true ): List<Double> { val huePopulation = List(size = 360, init = { 0.0 }).toMutableList() @@ -452,7 +477,7 @@ class ColorScheme( val population = populationByColor[entry.key]!! val cam = camByColor[entry.key]!! val hue = cam.hue.roundToInt() % 360 - if (cam.chroma <= MIN_CHROMA) { + if (filter && cam.chroma <= MIN_CHROMA) { continue } huePopulation[hue] = huePopulation[hue] + population diff --git a/packages/SystemUI/res/drawable/ic_chevron_icon.xml b/packages/SystemUI/res/drawable/ic_chevron_icon.xml index acbbbcb21503..d60cc8c74e80 100644 --- a/packages/SystemUI/res/drawable/ic_chevron_icon.xml +++ b/packages/SystemUI/res/drawable/ic_chevron_icon.xml @@ -15,14 +15,6 @@ ~ limitations under the License. --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="18dp" - android:height="31dp" - android:viewportWidth="18" - android:viewportHeight="31"> - <path - android:pathData="M0.0061,27.8986L2.6906,30.5831L17.9219,15.3518L2.6906,0.1206L0.0061,2.8051L12.5338,15.3518" - android:strokeAlpha="0.7" - android:fillColor="#FFFFFF" - android:fillAlpha="0.7"/> -</vector> +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal"> + <path android:fillColor="@android:color/white" android:pathData="M9.4,18 L8,16.6 12.6,12 8,7.4 9.4,6 15.4,12Z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/media_output_status_check.xml b/packages/SystemUI/res/drawable/media_output_status_check.xml index 5fbc42b245b8..3d64f83eabe3 100644 --- a/packages/SystemUI/res/drawable/media_output_status_check.xml +++ b/packages/SystemUI/res/drawable/media_output_status_check.xml @@ -21,6 +21,6 @@ android:viewportHeight="24" android:tint="?attr/colorControlNormal"> <path - android:fillColor="@color/media_dialog_item_main_content" - android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/> + android:fillColor="@android:color/white" + android:pathData="M12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM10.6,16.6 L17.65,9.55 16.25,8.15 10.6,13.8 7.75,10.95 6.35,12.35Z"/> </vector> diff --git a/packages/SystemUI/res/drawable/overlay_cancel.xml b/packages/SystemUI/res/drawable/overlay_cancel.xml index f9786e22b2d4..3fa12ddca70a 100644 --- a/packages/SystemUI/res/drawable/overlay_cancel.xml +++ b/packages/SystemUI/res/drawable/overlay_cancel.xml @@ -24,6 +24,6 @@ android:fillColor="?androidprv:attr/colorAccentTertiary" android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"/> <path - android:fillColor="?android:attr/textColorPrimary" + android:fillColor="?attr/overlayButtonTextColor" android:pathData="M23,10.41L21.59,9 16,14.59 10.41,9 9,10.41 14.59,16 9,21.59 10.41,23 16,17.41 21.59,23 23,21.59 17.41,16z"/> </vector> diff --git a/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml b/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml index 1122ca6bd4dc..1c09e81f92ca 100644 --- a/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml +++ b/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml @@ -12,6 +12,7 @@ android:layout_height="48dp" android:layout_marginTop="8dp" android:layout_marginStart="12dp" + android:paddingHorizontal="16dp" android:background="@drawable/overlay_button_background" android:text="@string/clipboard_edit_text_done" app:layout_constraintStart_toStartOf="parent" diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 085a5810608f..1712b4876b31 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -67,9 +67,9 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_marginStart="@dimen/overlay_offset_x" - android:layout_marginBottom="8dp" + android:layout_marginBottom="12dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="@id/actions_container_background" + app:layout_constraintBottom_toBottomOf="parent" android:elevation="7dp" app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end" app:layout_constraintTop_toTopOf="@id/clipboard_preview_top" diff --git a/packages/SystemUI/res/values/defaults.xml b/packages/SystemUI/res/values/defaults.xml deleted file mode 100644 index f96c178b119a..000000000000 --- a/packages/SystemUI/res/values/defaults.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2009, 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. - */ ---> -<resources> - <!-- Default for SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER. - To be set if the device wants to support out of the box QR code scanning experience --> - <string name="def_qr_code_component" translatable="false"></string> -</resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 0bc3594ef183..8ee29d7037db 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -30,10 +30,10 @@ <dimen name="navigation_bar_deadzone_size_max">32dp</dimen> <!-- dimensions for the navigation bar handle --> - <dimen name="navigation_handle_radius">1dp</dimen> - <dimen name="navigation_handle_bottom">6dp</dimen> + <dimen name="navigation_handle_radius">2dp</dimen> + <dimen name="navigation_handle_bottom">8dp</dimen> <dimen name="navigation_handle_sample_horizontal_margin">10dp</dimen> - <dimen name="navigation_home_handle_width">72dp</dimen> + <dimen name="navigation_home_handle_width">108dp</dimen> <!-- Size of the nav bar edge panels, should be greater to the edge sensitivity + the drag threshold --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 2426f017e20e..2c3d947ba9e2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2484,6 +2484,12 @@ <string name="clipboard_send_nearby_description">Send to nearby device</string> <!-- Text informing user that copied content is hidden [CHAR LIMIT=NONE] --> <string name="clipboard_text_hidden">Tap to view</string> + <!-- Accessibility announcement informing user that text has been copied [CHAR LIMIT=NONE] --> + <string name="clipboard_text_copied">Text copied</string> + <!-- Accessibility announcement informing user that text has been copied [CHAR LIMIT=NONE] --> + <string name="clipboard_image_copied">Image copied</string> + <!-- Accessibility announcement informing user that something has been copied [CHAR LIMIT=NONE] --> + <string name="clipboard_content_copied">Content copied</string> <!-- Generic "add" string [CHAR LIMIT=NONE] --> <string name="add">Add</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java index 804d14681812..12fa401d7fea 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java @@ -71,13 +71,16 @@ public class KeyguardHostViewController extends ViewController<KeyguardHostView> public void onTrustGrantedWithFlags(int flags, int userId) { if (userId != KeyguardUpdateMonitor.getCurrentUser()) return; boolean bouncerVisible = mView.isVisibleToUser(); + boolean temporaryAndRenewable = + (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) + != 0; boolean initiatedByUser = (flags & TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER) != 0; boolean dismissKeyguard = (flags & TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD) != 0; if (initiatedByUser || dismissKeyguard) { - if (mViewMediatorCallback.isScreenOn() + if ((mViewMediatorCallback.isScreenOn() || temporaryAndRenewable) && (bouncerVisible || dismissKeyguard)) { if (!bouncerVisible) { // The trust agent dismissed the keyguard without the user proving diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index f8c0590a8d75..cce516d981a5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -15,6 +15,8 @@ */ package com.android.keyguard; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; @@ -32,6 +34,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.app.Activity; import android.app.AlertDialog; +import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -676,7 +679,11 @@ public class KeyguardSecurityContainer extends FrameLayout { attempts, remaining); break; case USER_TYPE_WORK_PROFILE: - message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_profile, + message = mContext.getSystemService(DevicePolicyManager.class).getResources() + .getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE, + () -> mContext.getString( + R.string.kg_failed_attempts_almost_at_erase_profile, + attempts, remaining), attempts, remaining); break; } @@ -695,7 +702,10 @@ public class KeyguardSecurityContainer extends FrameLayout { attempts); break; case USER_TYPE_WORK_PROFILE: - message = mContext.getString(R.string.kg_failed_attempts_now_erasing_profile, + message = mContext.getSystemService(DevicePolicyManager.class).getResources() + .getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE, + () -> mContext.getString( + R.string.kg_failed_attempts_now_erasing_profile, attempts), attempts); break; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index e0f1b657e48c..13690f30ab3b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2417,7 +2417,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Triggers: final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant(); final boolean awakeKeyguard = mBouncerFullyShown || mUdfpsBouncerShowing - || (mKeyguardIsVisible && mDeviceInteractive && !mGoingToSleep + || (mKeyguardIsVisible && !mGoingToSleep && mStatusBarState != StatusBarState.SHADE_LOCKED); // Gates: diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index d79b1454514e..680b8bd70837 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -123,7 +123,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private float mHeightPixels; private float mWidthPixels; private int mBottomPaddingPx; - private int mScaledPaddingPx; + private int mDefaultPaddingPx; private boolean mShowUnlockIcon; private boolean mShowLockIcon; @@ -188,7 +188,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme protected void onViewAttached() { updateIsUdfpsEnrolled(); updateConfiguration(); - updateLockIconLocation(); updateKeyguardShowing(); mUserUnlockedWithBiometric = false; @@ -340,25 +339,27 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mWidthPixels = bounds.right; mHeightPixels = bounds.bottom; mBottomPaddingPx = getResources().getDimensionPixelSize(R.dimen.lock_icon_margin_bottom); + mDefaultPaddingPx = + getResources().getDimensionPixelSize(R.dimen.lock_icon_padding); mUnlockedLabel = mView.getContext().getResources().getString( R.string.accessibility_unlock_button); mLockedLabel = mView.getContext() .getResources().getString(R.string.accessibility_lock_icon); + updateLockIconLocation(); } private void updateLockIconLocation() { + final float scaleFactor = mAuthController.getScaleFactor(); + final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor); if (mUdfpsSupported) { - final int defaultPaddingPx = - getResources().getDimensionPixelSize(R.dimen.lock_icon_padding); - mScaledPaddingPx = (int) (defaultPaddingPx * mAuthController.getScaleFactor()); mView.setCenterLocation(mAuthController.getUdfpsLocation(), - mAuthController.getUdfpsRadius(), mScaledPaddingPx); + mAuthController.getUdfpsRadius(), scaledPadding); } else { mView.setCenterLocation( new PointF(mWidthPixels / 2, - mHeightPixels - mBottomPaddingPx - sLockIconRadiusPx), - sLockIconRadiusPx, mScaledPaddingPx); + mHeightPixels - ((mBottomPaddingPx + sLockIconRadiusPx) * scaleFactor)), + sLockIconRadiusPx * scaleFactor, scaledPadding); } } @@ -690,7 +691,6 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mExecutor.execute(() -> { updateIsUdfpsEnrolled(); updateConfiguration(); - updateLockIconLocation(); }); } @@ -707,7 +707,7 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @Override public void onUdfpsLocationChanged() { - updateLockIconLocation(); + updateUdfpsConfig(); } }; diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt index e51a63fbcd10..032a27a6fbcd 100644 --- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt +++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt @@ -54,7 +54,7 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @JvmField val displayInfo = DisplayInfo() - @JvmField protected var pendingRotationChange = false + @JvmField protected var pendingConfigChange = false @JvmField protected val paint = Paint() @JvmField protected val cutoutPath = Path() @@ -145,7 +145,7 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) open fun updateCutout() { - if (pendingRotationChange) { + if (pendingConfigChange) { return } cutoutPath.reset() @@ -225,7 +225,7 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { } protected open fun updateProtectionBoundingPath() { - if (pendingRotationChange) { + if (pendingConfigChange) { return } val m = Matrix() diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index ca731c50f48c..685c585a52be 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -53,6 +53,7 @@ import android.provider.Settings.Secure; import android.util.DisplayUtils; import android.util.Log; import android.util.Size; +import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayCutout.BoundsPosition; import android.view.DisplayInfo; @@ -151,12 +152,13 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab private SettingObserver mColorInversionSetting; private DelayableExecutor mExecutor; private Handler mHandler; - boolean mPendingRotationChange; + boolean mPendingConfigChange; @VisibleForTesting String mDisplayUniqueId; private int mTintColor = Color.BLACK; @VisibleForTesting protected DisplayDecorationSupport mHwcScreenDecorationSupport; + private Display.Mode mDisplayMode; private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback = new CameraAvailabilityListener.CameraTransitionCallback() { @@ -324,6 +326,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab mWindowManager = mContext.getSystemService(WindowManager.class); mDisplayManager = mContext.getSystemService(DisplayManager.class); mRotation = mContext.getDisplay().getRotation(); + mDisplayMode = mContext.getDisplay().getMode(); mDisplayUniqueId = mContext.getDisplay().getUniqueId(); mRoundedCornerResDelegate = new RoundedCornerResDelegate(mContext.getResources(), mDisplayUniqueId); @@ -349,8 +352,10 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab @Override public void onDisplayChanged(int displayId) { final int newRotation = mContext.getDisplay().getRotation(); + final Display.Mode newDisplayMode = mContext.getDisplay().getMode(); if ((mOverlays != null || mScreenDecorHwcWindow != null) - && mRotation != newRotation) { + && (mRotation != newRotation + || displayModeChanged(mDisplayMode, newDisplayMode))) { // We cannot immediately update the orientation. Otherwise // WindowManager is still deferring layout until it has finished dispatching // the config changes, which may cause divergence between what we draw @@ -358,10 +363,16 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab // Instead we wait until either: // - we are trying to redraw. This because WM resized our window and told us to. // - the config change has been dispatched, so WM is no longer deferring layout. - mPendingRotationChange = true; + mPendingConfigChange = true; if (DEBUG) { - Log.i(TAG, "Rotation changed, deferring " + newRotation + ", staying at " - + mRotation); + if (mRotation != newRotation) { + Log.i(TAG, "Rotation changed, deferring " + newRotation + + ", staying at " + mRotation); + } + if (displayModeChanged(mDisplayMode, newDisplayMode)) { + Log.i(TAG, "Resolution changed, deferring " + newDisplayMode + + ", staying at " + mDisplayMode); + } } if (mOverlays != null) { @@ -369,7 +380,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab if (mOverlays[i] != null) { final ViewGroup overlayView = mOverlays[i].getRootView(); overlayView.getViewTreeObserver().addOnPreDrawListener( - new RestartingPreDrawListener(overlayView, i, newRotation)); + new RestartingPreDrawListener( + overlayView, i, newRotation, newDisplayMode)); } } } @@ -379,7 +391,10 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab new RestartingPreDrawListener( mScreenDecorHwcWindow, -1, // Pass -1 for views with no specific position. - newRotation)); + newRotation, newDisplayMode)); + } + if (mScreenDecorHwcLayer != null) { + mScreenDecorHwcLayer.pendingConfigChange = true; } } @@ -435,7 +450,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab }; mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); - updateOrientation(); + updateConfiguration(); } @Nullable @@ -807,6 +822,17 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } } + private static boolean displayModeChanged(Display.Mode oldMode, Display.Mode newMode) { + if (oldMode == null) { + return true; + } + + // We purposely ignore refresh rate and id changes here, because we don't need to + // invalidate for those, and they can trigger the refresh rate to increase + return oldMode.getPhysicalWidth() != newMode.getPhysicalWidth() + || oldMode.getPhysicalHeight() != newMode.getPhysicalHeight(); + } + private int getOverlayWindowGravity(@BoundsPosition int pos) { final int rotated = getBoundPositionFromRotation(pos, mRotation); switch (rotated) { @@ -913,8 +939,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab mExecutor.execute(() -> { int oldRotation = mRotation; - mPendingRotationChange = false; - updateOrientation(); + mPendingConfigChange = false; + updateConfiguration(); if (DEBUG) Log.i(TAG, "onConfigChanged from rot " + oldRotation + " to " + mRotation); setupDecorations(); if (mOverlays != null) { @@ -941,7 +967,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab pw.println(" DEBUG_DISABLE_SCREEN_DECORATIONS:" + DEBUG_DISABLE_SCREEN_DECORATIONS); pw.println(" mIsPrivacyDotEnabled:" + isPrivacyDotEnabled()); pw.println(" isOnlyPrivacyDotInSwLayer:" + isOnlyPrivacyDotInSwLayer()); - pw.println(" mPendingRotationChange:" + mPendingRotationChange); + pw.println(" mPendingConfigChange:" + mPendingConfigChange); if (mHwcScreenDecorationSupport != null) { pw.println(" mHwcScreenDecorationSupport:"); pw.println(" format=" @@ -973,7 +999,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab mRoundedCornerResDelegate.dump(pw, args); } - private void updateOrientation() { + private void updateConfiguration() { Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(), "must call on " + mHandler.getLooper().getThread() + ", but was " + Thread.currentThread()); @@ -982,11 +1008,14 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab if (mRotation != newRotation) { mDotViewController.setNewRotation(newRotation); } + final Display.Mode newMod = mContext.getDisplay().getMode(); - if (!mPendingRotationChange && newRotation != mRotation) { + if (!mPendingConfigChange + && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) { mRotation = newRotation; + mDisplayMode = newMod; if (mScreenDecorHwcLayer != null) { - mScreenDecorHwcLayer.pendingRotationChange = false; + mScreenDecorHwcLayer.pendingConfigChange = false; mScreenDecorHwcLayer.updateRotation(mRotation); updateHwLayerRoundedCornerExistAndSize(); updateHwLayerRoundedCornerDrawable(); @@ -1197,7 +1226,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) @Override public void updateCutout() { - if (!isAttachedToWindow() || pendingRotationChange) { + if (!isAttachedToWindow() || pendingConfigChange) { return; } mPosition = getBoundPositionFromRotation(mInitialPosition, mRotation); @@ -1338,40 +1367,47 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab private final View mView; private final int mTargetRotation; + private final Display.Mode mTargetDisplayMode; // Pass -1 for ScreenDecorHwcLayer since it's a fullscreen window and has no specific // position. private final int mPosition; private RestartingPreDrawListener(View view, @BoundsPosition int position, - int targetRotation) { + int targetRotation, Display.Mode targetDisplayMode) { mView = view; mTargetRotation = targetRotation; + mTargetDisplayMode = targetDisplayMode; mPosition = position; } @Override public boolean onPreDraw() { mView.getViewTreeObserver().removeOnPreDrawListener(this); - - if (mTargetRotation == mRotation) { + if (mTargetRotation == mRotation + && !displayModeChanged(mDisplayMode, mTargetDisplayMode)) { if (DEBUG) { final String title = mPosition < 0 ? "ScreenDecorHwcLayer" : getWindowTitleByPos(mPosition); Log.i(TAG, title + " already in target rot " - + mTargetRotation + ", allow draw without restarting it"); + + mTargetRotation + " and in target resolution " + + mTargetDisplayMode.getPhysicalWidth() + "x" + + mTargetDisplayMode.getPhysicalHeight() + + ", allow draw without restarting it"); } return true; } - mPendingRotationChange = false; + mPendingConfigChange = false; // This changes the window attributes - we need to restart the traversal for them to // take effect. - updateOrientation(); + updateConfiguration(); if (DEBUG) { final String title = mPosition < 0 ? "ScreenDecorHwcLayer" : getWindowTitleByPos(mPosition); Log.i(TAG, title - + " restarting listener fired, restarting draw for rot " + mRotation); + + " restarting listener fired, restarting draw for rot " + mRotation + + ", resolution " + mDisplayMode.getPhysicalWidth() + "x" + + mDisplayMode.getPhysicalHeight()); } mView.invalidate(); return false; @@ -1379,8 +1415,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } /** - * A pre-draw listener, that validates that the rotation we draw in matches the displays - * rotation before continuing the draw. + * A pre-draw listener, that validates that the rotation and display resolution we draw in + * matches the display's rotation and resolution before continuing the draw. * * This is to prevent a race condition, where we have not received the display changed event * yet, and would thus draw in an old orientation. @@ -1396,10 +1432,20 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab @Override public boolean onPreDraw() { final int displayRotation = mContext.getDisplay().getRotation(); - if (displayRotation != mRotation && !mPendingRotationChange) { + final Display.Mode displayMode = mContext.getDisplay().getMode(); + if (displayRotation != mRotation && displayModeChanged(mDisplayMode, displayMode) + && !mPendingConfigChange) { if (DEBUG) { - Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot " - + displayRotation + ". Restarting draw"); + if (displayRotation != mRotation) { + Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot " + + displayRotation + ". Restarting draw"); + } + if (displayModeChanged(mDisplayMode, displayMode)) { + Log.i(TAG, "Drawing at " + mDisplayMode.getPhysicalWidth() + + "x" + mDisplayMode.getPhysicalHeight() + ", but display is at " + + displayMode.getPhysicalWidth() + "x" + + displayMode.getPhysicalHeight() + ". Restarting draw"); + } } mView.invalidate(); return false; diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index 3d0c08bb5237..fe6dbe5de8f0 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -46,6 +46,8 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.wm.shell.animation.FlingAnimationUtils; +import java.util.function.Consumer; + public class SwipeHelper implements Gefingerpoken { static final String TAG = "com.android.systemui.SwipeHelper"; private static final boolean DEBUG = false; @@ -399,7 +401,7 @@ public class SwipeHelper implements Gefingerpoken { * @param useAccelerateInterpolator Should an accelerating Interpolator be used * @param fixedDuration If not 0, this exact duration will be taken */ - public void dismissChild(final View animView, float velocity, final Runnable endAction, + public void dismissChild(final View animView, float velocity, final Consumer<Boolean> endAction, long delay, boolean useAccelerateInterpolator, long fixedDuration, boolean isDismissAll) { final boolean canBeDismissed = mCallback.canChildBeDismissed(animView); @@ -487,7 +489,7 @@ public class SwipeHelper implements Gefingerpoken { resetSwipeState(); } if (endAction != null) { - endAction.run(); + endAction.accept(mCancelled); } if (!mDisableHwLayers) { animView.setLayerType(View.LAYER_TYPE_NONE, null); diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index 56fbe6d9bf14..6b859763eb6f 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -102,6 +102,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_SYSTEM_ALERT_WINDOW, AppOpsManager.OP_RECORD_AUDIO, + AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, AppOpsManager.OP_PHONE_CALL_MICROPHONE, AppOpsManager.OP_COARSE_LOCATION, AppOpsManager.OP_FINE_LOCATION @@ -375,7 +376,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s,%d,%d)", code, uid, packageName, Boolean.toString(active), attributionChainId, attributionFlags)); } - if (attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE + if (active && attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE && attributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR) == 0 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_TRUSTED) == 0) { @@ -535,7 +536,8 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon } private boolean isOpMicrophone(int op) { - return op == AppOpsManager.OP_RECORD_AUDIO || op == AppOpsManager.OP_PHONE_CALL_MICROPHONE; + return op == AppOpsManager.OP_RECORD_AUDIO || op == AppOpsManager.OP_PHONE_CALL_MICROPHONE + || op == AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; } protected class H extends Handler { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 75339aaa843d..17396469c10a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -484,7 +484,13 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba if (mFaceProps == null || mFaceAuthSensorLocation == null) { return null; } - return new PointF(mFaceAuthSensorLocation.x, mFaceAuthSensorLocation.y); + DisplayInfo displayInfo = new DisplayInfo(); + mContext.getDisplay().getDisplayInfo(displayInfo); + final float scaleFactor = android.util.DisplayUtils.getPhysicalPixelDisplaySizeRatio( + mStableDisplaySize.x, mStableDisplaySize.y, displayInfo.getNaturalWidth(), + displayInfo.getNaturalHeight()); + return new PointF(mFaceAuthSensorLocation.x * scaleFactor, + mFaceAuthSensorLocation.y * scaleFactor); } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 7657269643aa..9febaa01eed7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -51,7 +51,6 @@ import android.view.accessibility.AccessibilityManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.LatencyTracker; -import com.android.keyguard.ActiveUnlockConfig; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; @@ -808,10 +807,6 @@ public class UdfpsController implements DozeReceiver { if (!mKeyguardUpdateMonitor.isFaceDetectionRunning()) { mKeyguardUpdateMonitor.requestFaceAuth(/* userInitiatedRequest */ false); } - - mKeyguardUpdateMonitor.requestActiveUnlock( - ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT, - "udfpsFingerDown"); } mOnFingerDown = true; if (mAlternateTouchProvider != null) { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java index 50550567ac16..c3dd29e91ea6 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java @@ -302,6 +302,7 @@ public class ClipboardOverlayController { mExitAnimator.cancel(); } reset(); + String accessibilityAnnouncement; boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null && clipData.getDescription().getExtras() @@ -310,6 +311,7 @@ public class ClipboardOverlayController { showTextPreview( mContext.getResources().getString(R.string.clipboard_overlay_text_copied), mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) { ClipData.Item item = clipData.getItemAt(0); if (item.getTextLinks() != null) { @@ -321,13 +323,18 @@ public class ClipboardOverlayController { } else { showEditableText(item.getText(), false); } + accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied); } else if (clipData.getItemAt(0).getUri() != null) { - // How to handle non-image URIs? - showEditableImage(clipData.getItemAt(0).getUri(), isSensitive); + if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) { + accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied); + } else { + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); + } } else { showTextPreview( mContext.getResources().getString(R.string.clipboard_overlay_text_copied), mTextPreview); + accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied); } Intent remoteCopyIntent = getRemoteCopyIntent(clipData); // Only show remote copy if it's available. @@ -344,7 +351,12 @@ public class ClipboardOverlayController { } else { mRemoteCopyChip.setVisibility(View.GONE); } - withWindowAttached(() -> mView.post(this::animateIn)); + withWindowAttached(() -> { + updateInsets( + mWindowManager.getCurrentWindowMetrics().getWindowInsets()); + mView.post(this::animateIn); + mView.announceForAccessibility(accessibilityAnnouncement); + }); mTimeoutHandler.resetTimeout(); } @@ -476,33 +488,46 @@ public class ClipboardOverlayController { textView.setOnClickListener(listener); } - private void showEditableImage(Uri uri, boolean isSensitive) { - mEditChip.setAlpha(1f); - mActionContainerBackground.setVisibility(View.VISIBLE); + private boolean tryShowEditableImage(Uri uri, boolean isSensitive) { View.OnClickListener listener = v -> editImage(uri); + ContentResolver resolver = mContext.getContentResolver(); + String mimeType = resolver.getType(uri); + boolean isEditableImage = mimeType != null && mimeType.startsWith("image"); if (isSensitive) { showSinglePreview(mHiddenImagePreview); - mHiddenImagePreview.setOnClickListener(listener); - } else { - showSinglePreview(mImagePreview); - ContentResolver resolver = mContext.getContentResolver(); + if (isEditableImage) { + mHiddenImagePreview.setOnClickListener(listener); + } + } else if (isEditableImage) { // if the MIMEtype is image, try to load try { int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale); // The width of the view is capped, height maintains aspect ratio, so allow it to be // taller if needed. Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null); + showSinglePreview(mImagePreview); mImagePreview.setImageBitmap(thumbnail); + mImagePreview.setOnClickListener(listener); } catch (IOException e) { Log.e(TAG, "Thumbnail loading failed", e); showTextPreview( mContext.getResources().getString(R.string.clipboard_overlay_text_copied), mTextPreview); + isEditableImage = false; } - mImagePreview.setOnClickListener(listener); + } else { + showTextPreview( + mContext.getResources().getString(R.string.clipboard_overlay_text_copied), + mTextPreview); } - mEditChip.setOnClickListener(listener); - mEditChip.setContentDescription( - mContext.getString(R.string.clipboard_edit_image_description)); + if (isEditableImage) { + mEditChip.setVisibility(View.VISIBLE); + mEditChip.setAlpha(1f); + mActionContainerBackground.setVisibility(View.VISIBLE); + mEditChip.setOnClickListener(listener); + mEditChip.setContentDescription( + mContext.getString(R.string.clipboard_edit_image_description)); + } + return isEditableImage; } private Intent getRemoteCopyIntent(ClipData clipData) { @@ -526,6 +551,9 @@ public class ClipboardOverlayController { } private void animateOut() { + if (mExitAnimator != null && mExitAnimator.isRunning()) { + return; + } Animator anim = getExitAnimation(); anim.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index a4f9f3a9bc08..6a9aaf865251 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -472,7 +472,6 @@ class ControlViewHolder( updateContentDescription() status.setTextColor(color) - chevronIcon.imageTintList = color control?.getCustomIcon()?.let { icon.setImageIcon(it) @@ -495,10 +494,13 @@ class ControlViewHolder( icon.imageTintList = color } } + + chevronIcon.imageTintList = icon.imageTintList } private fun setEnabled(enabled: Boolean) { - status.setEnabled(enabled) - icon.setEnabled(enabled) + status.isEnabled = enabled + icon.isEnabled = enabled + chevronIcon.isEnabled = enabled } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index e963c39329ca..afa7d5e0a9c4 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -151,7 +151,7 @@ public class Flags { /***************************************/ // 900 - media public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true); - public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, true); + public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, false); public static final BooleanFlag MEDIA_NEARBY_DEVICES = new BooleanFlag(903, true); public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 404e5311961b..6dfc5e192abb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -44,6 +44,7 @@ import com.android.systemui.shared.system.QuickStepContract import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController import com.android.systemui.shared.system.smartspace.SmartspaceState +import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.KeyguardStateController @@ -142,7 +143,8 @@ class KeyguardUnlockAnimationController @Inject constructor( private val keyguardViewController: KeyguardViewController, private val featureFlags: FeatureFlags, private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>, - private val statusBarStateController: SysuiStatusBarStateController + private val statusBarStateController: SysuiStatusBarStateController, + private val notificationShadeWindowController: NotificationShadeWindowController ) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() { interface KeyguardUnlockAnimationListener { @@ -362,6 +364,9 @@ class KeyguardUnlockAnimationController @Inject constructor( */ fun canPerformInWindowLauncherAnimations(): Boolean { return isNexusLauncherUnderneath() && + // If the launcher is underneath, but we're about to launch an activity, don't do + // the animations since they won't be visible. + !notificationShadeWindowController.isLaunchingActivity && launcherUnlockController != null && !keyguardStateController.isDismissingFromSwipe && // Temporarily disable for foldables since foldable launcher has two first pages, @@ -413,7 +418,6 @@ class KeyguardUnlockAnimationController @Inject constructor( (lockscreenSmartspace as BcSmartspaceDataPlugin.SmartspaceView?)?.selectedPage ?: -1 try { - // Let the launcher know to prepare for this animation. launcherUnlockController?.prepareForUnlock( willUnlockWithSmartspaceTransition, /* willAnimateSmartspace */ diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index f72f1bb47468..1e7a292dadba 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -230,6 +230,18 @@ public class LogModule { return factory.create("MediaBrowser", 100); } + /** + * Provides a buffer for updates to the media carousel. + * + * See {@link com.android.systemui.media.MediaCarouselController}. + */ + @Provides + @SysUISingleton + @MediaCarouselControllerLog + public static LogBuffer provideMediaCarouselControllerBuffer(LogBufferFactory factory) { + return factory.create("MediaCarouselCtlrLog", 20); + } + /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java index 84ff538677b0..b03655a543f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java @@ -14,17 +14,22 @@ * limitations under the License. */ -package com.android.systemui.statusbar.phone; +package com.android.systemui.log.dagger; -import com.android.systemui.dagger.SysUISingleton; +import static java.lang.annotation.RetentionPolicy.RUNTIME; -import dagger.Binds; -import dagger.Module; +import com.android.systemui.log.LogBuffer; -/** Provides a {@link NotifActivityLaunchEvents} in {@link SysUISingleton} scope. */ -@Module -public abstract class NotifActivityLaunchEventsModule { - @Binds - abstract NotifActivityLaunchEvents bindLaunchEvents( - StatusBarNotificationActivityStarter.LaunchEventsEmitter impl); +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** + * A {@link LogBuffer} for {@link com.android.systemui.media.MediaCarouselController} + */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface MediaCarouselControllerLog { } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 3483bc39b943..8a104c42068e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -59,7 +59,8 @@ class MediaCarouselController @Inject constructor( falsingCollector: FalsingCollector, falsingManager: FalsingManager, dumpManager: DumpManager, - private val logger: MediaUiEventLogger + private val logger: MediaUiEventLogger, + private val debugLogger: MediaCarouselControllerLogger ) : Dumpable { /** * The current width of the carousel @@ -439,12 +440,16 @@ class MediaCarouselController @Inject constructor( newPlayer.mediaViewHolder?.player?.setLayoutParams(lp) newPlayer.bindPlayer(data, key) newPlayer.setListening(currentlyExpanded) - MediaPlayerData.addMediaPlayer(key, data, newPlayer, systemClock, isSsReactivated) + MediaPlayerData.addMediaPlayer( + key, data, newPlayer, systemClock, isSsReactivated, debugLogger + ) updatePlayerToState(newPlayer, noAnimation = true) reorderAllPlayers(curVisibleMediaKey) } else { existingPlayer.bindPlayer(data, key) - MediaPlayerData.addMediaPlayer(key, data, existingPlayer, systemClock, isSsReactivated) + MediaPlayerData.addMediaPlayer( + key, data, existingPlayer, systemClock, isSsReactivated, debugLogger + ) if (isReorderingAllowed || shouldScrollToActivePlayer) { reorderAllPlayers(curVisibleMediaKey) } else { @@ -475,7 +480,8 @@ class MediaCarouselController @Inject constructor( val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() existingSmartspaceMediaKey?.let { - MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey) + val removedPlayer = MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey) + removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) } } val newRecs = mediaControlPanelFactory.get() @@ -488,7 +494,9 @@ class MediaCarouselController @Inject constructor( newRecs.bindRecommendation(data) val curVisibleMediaKey = MediaPlayerData.playerKeys() .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex) - MediaPlayerData.addMediaRecommendation(key, data, newRecs, shouldPrioritize, systemClock) + MediaPlayerData.addMediaRecommendation( + key, data, newRecs, shouldPrioritize, systemClock, debugLogger + ) updatePlayerToState(newRecs, noAnimation = true) reorderAllPlayers(curVisibleMediaKey) updatePageIndicator() @@ -845,7 +853,8 @@ class MediaCarouselController @Inject constructor( uid, interactedSubcardRank, interactedSubcardCardinality, - receivedLatencyMillis + receivedLatencyMillis, + null // Media cards cannot have subcards. ) /* ktlint-disable max-line-length */ if (DEBUG) { @@ -882,7 +891,8 @@ class MediaCarouselController @Inject constructor( override fun dump(pw: PrintWriter, args: Array<out String>) { pw.apply { println("keysNeedRemoval: $keysNeedRemoval") - println("playerKeys: ${MediaPlayerData.playerKeys()}") + println("dataKeys: ${MediaPlayerData.dataKeys()}") + println("playerSortKeys: ${MediaPlayerData.playerKeys()}") println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}") println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}") println("current size: $currentCarouselWidth x $currentCarouselHeight") @@ -945,9 +955,13 @@ internal object MediaPlayerData { data: MediaData, player: MediaControlPanel, clock: SystemClock, - isSsReactivated: Boolean + isSsReactivated: Boolean, + debugLogger: MediaCarouselControllerLogger? = null ) { - removeMediaPlayer(key) + val removedPlayer = removeMediaPlayer(key) + if (removedPlayer != null && removedPlayer != player) { + debugLogger?.logPotentialMemoryLeak(key) + } val sortKey = MediaSortKey(isSsMediaRec = false, data, clock.currentTimeMillis(), isSsReactivated = isSsReactivated) mediaData.put(key, sortKey) @@ -959,10 +973,14 @@ internal object MediaPlayerData { data: SmartspaceMediaData, player: MediaControlPanel, shouldPrioritize: Boolean, - clock: SystemClock + clock: SystemClock, + debugLogger: MediaCarouselControllerLogger? = null ) { shouldPrioritizeSs = shouldPrioritize - removeMediaPlayer(key) + val removedPlayer = removeMediaPlayer(key) + if (removedPlayer != null && removedPlayer != player) { + debugLogger?.logPotentialMemoryLeak(key) + } val sortKey = MediaSortKey(isSsMediaRec = true, EMPTY.copy(isPlaying = false), clock.currentTimeMillis(), isSsReactivated = true) mediaData.put(key, sortKey) @@ -970,13 +988,18 @@ internal object MediaPlayerData { smartspaceMediaData = data } - fun moveIfExists(oldKey: String?, newKey: String) { + fun moveIfExists( + oldKey: String?, + newKey: String, + debugLogger: MediaCarouselControllerLogger? = null + ) { if (oldKey == null || oldKey == newKey) { return } mediaData.remove(oldKey)?.let { - removeMediaPlayer(newKey) + val removedPlayer = removeMediaPlayer(newKey) + removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) } mediaData.put(newKey, it) } } @@ -1004,6 +1027,8 @@ internal object MediaPlayerData { fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) } + fun dataKeys() = mediaData.keys + fun players() = mediaPlayers.values fun playerKeys() = mediaPlayers.keys diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt new file mode 100644 index 000000000000..04ebd5a71137 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt @@ -0,0 +1,45 @@ +/* + * 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.systemui.media + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.dagger.MediaCarouselControllerLog +import javax.inject.Inject + +/** A debug logger for [MediaCarouselController]. */ +@SysUISingleton +class MediaCarouselControllerLogger @Inject constructor( + @MediaCarouselControllerLog private val buffer: LogBuffer +) { + /** + * Log that there might be a potential memory leak for the [MediaControlPanel] and/or + * [MediaViewController] related to [key]. + */ + fun logPotentialMemoryLeak(key: String) = buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { + "Potential memory leak: " + + "Removing control panel for $str1 from map without calling #onDestroy" + } + ) +} + +private const val TAG = "MediaCarouselCtlrLog" diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index b9601420bb26..6ef25046d328 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -75,6 +75,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.monet.ColorScheme; +import com.android.systemui.monet.Style; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.shared.system.SysUiStatsLog; @@ -627,7 +628,7 @@ public class MediaControlPanel { if (artworkIcon != null) { WallpaperColors wallpaperColors = WallpaperColors .fromBitmap(artworkIcon.getBitmap()); - mutableColorScheme = new ColorScheme(wallpaperColors, true); + mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT); artwork = getScaledBackground(artworkIcon, width, height); isArtworkBound = true; } else { @@ -637,7 +638,8 @@ public class MediaControlPanel { try { Drawable icon = mContext.getPackageManager() .getApplicationIcon(data.getPackageName()); - mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true); + mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true, + Style.CONTENT); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e); } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index fc8d38d59d59..d4c4f2165339 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -22,8 +22,11 @@ import android.os.SystemProperties import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -42,7 +45,9 @@ val RESUME_MEDIA_TIMEOUT = SystemProperties class MediaTimeoutListener @Inject constructor( private val mediaControllerFactory: MediaControllerFactory, @Main private val mainExecutor: DelayableExecutor, - private val logger: MediaTimeoutLogger + private val logger: MediaTimeoutLogger, + statusBarStateController: SysuiStatusBarStateController, + private val systemClock: SystemClock ) : MediaDataManager.Listener { private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf() @@ -62,6 +67,24 @@ class MediaTimeoutListener @Inject constructor( */ lateinit var stateCallback: (String, PlaybackState) -> Unit + init { + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onDozingChanged(isDozing: Boolean) { + if (!isDozing) { + // Check whether any timeouts should have expired + mediaListeners.forEach { (key, listener) -> + if (listener.cancellation != null && + listener.expiration <= systemClock.elapsedRealtime()) { + // We dozed too long - timeout now, and cancel the pending one + listener.expireMediaTimeout(key, "timeout happened while dozing") + listener.doTimeout() + } + } + } + } + }) + } + override fun onMediaDataLoaded( key: String, oldKey: String?, @@ -131,6 +154,7 @@ class MediaTimeoutListener @Inject constructor( var lastState: PlaybackState? = null var resumption: Boolean? = null var destroyed = false + var expiration = Long.MAX_VALUE var mediaData: MediaData = data set(value) { @@ -150,7 +174,8 @@ class MediaTimeoutListener @Inject constructor( // Resume controls may have null token private var mediaController: MediaController? = null - private var cancellation: Runnable? = null + var cancellation: Runnable? = null + private set fun Int.isPlaying() = isPlayingState(this) fun isPlaying() = lastState?.state?.isPlaying() ?: false @@ -216,12 +241,9 @@ class MediaTimeoutListener @Inject constructor( } else { PAUSED_MEDIA_TIMEOUT } + expiration = systemClock.elapsedRealtime() + timeout cancellation = mainExecutor.executeDelayed({ - cancellation = null - logger.logTimeout(key) - timedOut = true - // this event is async, so it's safe even when `dispatchEvents` is false - timeoutCallback(key, timedOut) + doTimeout() }, timeout) } else { expireMediaTimeout(key, "playback started - $state, $key") @@ -232,11 +254,21 @@ class MediaTimeoutListener @Inject constructor( } } - private fun expireMediaTimeout(mediaKey: String, reason: String) { + fun doTimeout() { + cancellation = null + logger.logTimeout(key) + timedOut = true + expiration = Long.MAX_VALUE + // this event is async, so it's safe even when `dispatchEvents` is false + timeoutCallback(key, timedOut) + } + + fun expireMediaTimeout(mediaKey: String, reason: String) { cancellation?.apply { logger.logTimeoutCancelled(mediaKey, reason) run() } + expiration = Long.MAX_VALUE cancellation = null } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 8dcca3d55c28..1a727f8c3323 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -76,6 +76,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements private static final String PREF_NAME = "MediaOutputDialog"; private static final String PREF_IS_LE_BROADCAST_FIRST_LAUNCH = "PrefIsLeBroadcastFirstLaunch"; private static final boolean DEBUG = true; + private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000; private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); private final RecyclerView.LayoutManager mLayoutManager; @@ -119,7 +120,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements Log.d(TAG, "onBroadcastStarted(), reason = " + reason + ", broadcastId = " + broadcastId); } - mMainThreadHandler.post(() -> startLeBroadcastDialog()); + mMainThreadHandler.post(() -> handleLeBroadcastStarted()); } @Override @@ -127,7 +128,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements if (DEBUG) { Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); } - handleLeBroadcastStartFailed(); + mMainThreadHandler.postDelayed(() -> handleLeBroadcastStartFailed(), + HANDLE_BROADCAST_FAILED_DELAY); } @Override @@ -137,7 +139,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId + ", metadata = " + metadata); } - mMainThreadHandler.post(() -> refresh()); + mMainThreadHandler.post(() -> handleLeBroadcastMetadataChanged()); } @Override @@ -146,7 +148,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements Log.d(TAG, "onBroadcastStopped(), reason = " + reason + ", broadcastId = " + broadcastId); } - mMainThreadHandler.post(() -> refresh()); + mMainThreadHandler.post(() -> handleLeBroadcastStopped()); } @Override @@ -154,7 +156,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements if (DEBUG) { Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); } - mMainThreadHandler.post(() -> refresh()); + mMainThreadHandler.post(() -> handleLeBroadcastStopFailed()); } @Override @@ -163,7 +165,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements Log.d(TAG, "onBroadcastUpdated(), reason = " + reason + ", broadcastId = " + broadcastId); } - mMainThreadHandler.post(() -> refresh()); + mMainThreadHandler.post(() -> handleLeBroadcastUpdated()); } @Override @@ -172,7 +174,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements Log.d(TAG, "onBroadcastUpdateFailed(), reason = " + reason + ", broadcastId = " + broadcastId); } - mMainThreadHandler.post(() -> refresh()); + mMainThreadHandler.post(() -> handleLeBroadcastUpdateFailed()); } @Override @@ -384,10 +386,34 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements Bitmap.createScaledBitmap(bitmap, size, size, false)); } - protected void handleLeBroadcastStartFailed() { + public void handleLeBroadcastStarted() { + startLeBroadcastDialog(); + } + + public void handleLeBroadcastStartFailed() { mStopButton.setText(R.string.media_output_broadcast_start_failed); mStopButton.setEnabled(false); - mMainThreadHandler.postDelayed(() -> refresh(), 3000); + refresh(); + } + + public void handleLeBroadcastMetadataChanged() { + refresh(); + } + + public void handleLeBroadcastStopped() { + refresh(); + } + + public void handleLeBroadcastStopFailed() { + refresh(); + } + + public void handleLeBroadcastUpdated() { + refresh(); + } + + public void handleLeBroadcastUpdateFailed() { + refresh(); } protected void startLeBroadcast() { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java index dd4f1d6c9015..8f065461c22d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -61,6 +61,10 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { private ImageView mBroadcastCodeEdit; private AlertDialog mAlertDialog; private TextView mBroadcastErrorMessage; + private int mRetryCount = 0; + private String mCurrentBroadcastName; + private String mCurrentBroadcastCode; + private boolean mIsStopbyUpdateBroadcastCode = false; static final int METADATA_BROADCAST_NAME = 0; static final int METADATA_BROADCAST_CODE = 1; @@ -144,8 +148,6 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { //init UI component mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view); - //Set the QR code view - setQrCodeView(); mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info); mBroadcastNotify.setOnClickListener(v -> { @@ -171,8 +173,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { launchBroadcastUpdatedDialog(true, mBroadcastCode.getText().toString()); }); - mBroadcastName.setText(getBroadcastMetadataInfo(METADATA_BROADCAST_NAME)); - mBroadcastCode.setText(getBroadcastMetadataInfo(METADATA_BROADCAST_CODE)); + refreshUi(); + } + + private void refreshUi() { + setQrCodeView(); + + mCurrentBroadcastName = getBroadcastMetadataInfo(METADATA_BROADCAST_NAME); + mCurrentBroadcastCode = getBroadcastMetadataInfo(METADATA_BROADCAST_CODE); + mBroadcastName.setText(mCurrentBroadcastName); + mBroadcastCode.setText(mCurrentBroadcastCode); } private void inflateBroadcastInfoArea() { @@ -239,52 +249,99 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { } if (isBroadcastCode) { - handleBroadcastCodeUpdated(updatedString); + /* If the user wants to update the Broadcast Code, the Broadcast session should be + * stopped then used the new Broadcast code to start the Broadcast. + */ + mIsStopbyUpdateBroadcastCode = true; + mMediaOutputController.setBroadcastCode(updatedString); + if (!mMediaOutputController.stopBluetoothLeBroadcast()) { + handleLeBroadcastStopFailed(); + return; + } } else { - handleBroadcastNameUpdated(updatedString); + /* If the user wants to update the Broadcast Name, we don't need to stop the Broadcast + * session. Only use the new Broadcast name to update the broadcast session. + */ + mMediaOutputController.setBroadcastName(updatedString); + if (!mMediaOutputController.updateBluetoothLeBroadcast()) { + handleLeBroadcastUpdateFailed(); + } } } - private void handleBroadcastNameUpdated(String name) { - // TODO(b/230473995) Add the retry mechanism and error handling when update fails - String currentName = mMediaOutputController.getBroadcastName(); - int retryCount = MAX_BROADCAST_INFO_UPDATE; - mMediaOutputController.setBroadcastName(name); - if (!mMediaOutputController.updateBluetoothLeBroadcast()) { - mMediaOutputController.setBroadcastName(currentName); - handleLeUpdateBroadcastFailed(retryCount); + @Override + public void handleLeBroadcastStarted() { + mRetryCount = 0; + if (mAlertDialog != null) { + mAlertDialog.dismiss(); } + refreshUi(); } - private void handleBroadcastCodeUpdated(String newPassword) { - // TODO(b/230473995) Add the retry mechanism and error handling when update fails - String currentPassword = mMediaOutputController.getBroadcastCode(); - int retryCount = MAX_BROADCAST_INFO_UPDATE; - if (!mMediaOutputController.stopBluetoothLeBroadcast()) { - mMediaOutputController.setBroadcastCode(currentPassword); - handleLeUpdateBroadcastFailed(retryCount); - return; + @Override + public void handleLeBroadcastStartFailed() { + mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode); + mRetryCount++; + + handleUpdateFailedUi(); + } + + @Override + public void handleLeBroadcastMetadataChanged() { + refreshUi(); + } + + @Override + public void handleLeBroadcastUpdated() { + mRetryCount = 0; + if (mAlertDialog != null) { + mAlertDialog.dismiss(); } + refreshUi(); + } - mMediaOutputController.setBroadcastCode(newPassword); - if (!mMediaOutputController.startBluetoothLeBroadcast()) { - mMediaOutputController.setBroadcastCode(currentPassword); - handleLeUpdateBroadcastFailed(retryCount); - return; + @Override + public void handleLeBroadcastUpdateFailed() { + //Change the value in shared preferences back to it original value + mMediaOutputController.setBroadcastName(mCurrentBroadcastName); + mRetryCount++; + + handleUpdateFailedUi(); + } + + @Override + public void handleLeBroadcastStopped() { + if (mIsStopbyUpdateBroadcastCode) { + mIsStopbyUpdateBroadcastCode = false; + mRetryCount = 0; + if (!mMediaOutputController.startBluetoothLeBroadcast()) { + handleLeBroadcastStartFailed(); + return; + } + } else { + dismiss(); } + } + + @Override + public void handleLeBroadcastStopFailed() { + //Change the value in shared preferences back to it original value + mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode); + mRetryCount++; - mAlertDialog.dismiss(); + handleUpdateFailedUi(); } - private void handleLeUpdateBroadcastFailed(int retryCount) { + private void handleUpdateFailedUi() { final Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); mBroadcastErrorMessage.setVisibility(View.VISIBLE); - if (retryCount < MAX_BROADCAST_INFO_UPDATE) { + if (mRetryCount < MAX_BROADCAST_INFO_UPDATE) { if (positiveBtn != null) { positiveBtn.setEnabled(true); } mBroadcastErrorMessage.setText(R.string.media_output_broadcast_update_error); } else { + mRetryCount = 0; mBroadcastErrorMessage.setText(R.string.media_output_broadcast_last_update_error); } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 58b6ad3e51e8..9329b1bfc2a3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -573,6 +573,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } boolean addDeviceToPlayMedia(MediaDevice device) { + mMetricLogger.logInteractionExpansion(device); return mLocalMediaManager.addDeviceToPlayMedia(device); } @@ -613,6 +614,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } void releaseSession() { + mMetricLogger.logInteractionStopCasting(); mLocalMediaManager.releaseSession(); } @@ -627,6 +629,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } void adjustVolume(MediaDevice device, int volume) { + mMetricLogger.logInteractionAdjustVolume(device); ThreadUtils.postOnBackgroundThread(() -> { device.requestSetVolume(volume); }); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java index ac0295ed6d14..5d7af522176a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java @@ -96,6 +96,49 @@ public class MediaOutputMetricLogger { } /** + * Do the metric logging of volume adjustment. + * @param source the device been adjusted + */ + public void logInteractionAdjustVolume(MediaDevice source) { + if (DEBUG) { + Log.d(TAG, "logInteraction - AdjustVolume"); + } + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__ADJUST_VOLUME, + getInteractionDeviceType(source)); + } + + /** + * Do the metric logging of stop casting. + */ + public void logInteractionStopCasting() { + if (DEBUG) { + Log.d(TAG, "logInteraction - Stop casting"); + } + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__STOP_CASTING, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE); + } + + /** + * Do the metric logging of device expansion. + */ + public void logInteractionExpansion(MediaDevice source) { + if (DEBUG) { + Log.d(TAG, "logInteraction - Expansion"); + } + + SysUiStatsLog.write( + SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT, + SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__EXPANSION, + getInteractionDeviceType(source)); + } + + /** * Do the metric logging of content switching failure. * @param deviceList media device list for device count updating * @param reason the reason of content switching failure @@ -185,6 +228,27 @@ public class MediaOutputMetricLogger { } } + private int getInteractionDeviceType(MediaDevice device) { + switch (device.getDeviceType()) { + case MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE: + return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__BUILTIN_SPEAKER; + case MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE: + return SysUiStatsLog + .MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__WIRED_3POINT5_MM_AUDIO; + case MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE: + return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__USB_C_AUDIO; + case MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE: + return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__BLUETOOTH; + case MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE: + return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__REMOTE_SINGLE; + case MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE: + return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__REMOTE_GROUP; + default: + return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE; + } + } + + private int getLoggingSwitchOpSubResult(int reason) { switch (reason) { case REASON_REJECTED: diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java index d27b71673ce5..622f5a279a5f 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java @@ -273,9 +273,8 @@ public class KeyButtonView extends ImageView implements ButtonInterface { mLongClicked = false; setPressed(true); - // Use raw X and Y to detect gestures in case a parent changes the x and y values - mTouchDownX = (int) ev.getRawX(); - mTouchDownY = (int) ev.getRawY(); + mTouchDownX = (int) ev.getX(); + mTouchDownY = (int) ev.getY(); if (mCode != KEYCODE_UNKNOWN) { sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); } else { @@ -289,8 +288,8 @@ public class KeyButtonView extends ImageView implements ButtonInterface { postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); break; case MotionEvent.ACTION_MOVE: - x = (int)ev.getRawX(); - y = (int)ev.getRawY(); + x = (int) ev.getX(); + y = (int) ev.getY(); float slop = QuickStepContract.getQuickStepTouchSlopPx(getContext()); if (Math.abs(x - mTouchDownX) > slop || Math.abs(y - mTouchDownY) > slop) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java index 33e6aa46724b..913b65289e12 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java @@ -36,8 +36,8 @@ public class NavigationHandle extends View implements ButtonInterface { protected final Paint mPaint = new Paint(); private @ColorInt final int mLightColor; private @ColorInt final int mDarkColor; - protected final int mRadius; - protected final int mBottom; + protected final float mRadius; + protected final float mBottom; private boolean mRequiresInvalidate; public NavigationHandle(Context context) { @@ -47,8 +47,8 @@ public class NavigationHandle extends View implements ButtonInterface { public NavigationHandle(Context context, AttributeSet attr) { super(context, attr); final Resources res = context.getResources(); - mRadius = res.getDimensionPixelSize(R.dimen.navigation_handle_radius); - mBottom = res.getDimensionPixelSize(R.dimen.navigation_handle_bottom); + mRadius = res.getDimension(R.dimen.navigation_handle_radius); + mBottom = res.getDimension(R.dimen.navigation_handle_bottom); final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme); final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme); @@ -75,9 +75,9 @@ public class NavigationHandle extends View implements ButtonInterface { // Draw that bar int navHeight = getHeight(); - int height = mRadius * 2; + float height = mRadius * 2; int width = getWidth(); - int y = (navHeight - mBottom - height); + float y = (navHeight - mBottom - height); canvas.drawRoundRect(0, y, width, y + height, mRadius, mRadius, mPaint); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java index 71c8a2c1e6ca..88622aadd543 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java @@ -44,33 +44,33 @@ public class QuickswitchOrientedNavHandle extends NavigationHandle { } public RectF computeHomeHandleBounds() { - int left; - int top; - int bottom; - int right; - int radiusOffset = mRadius * 2; + float left; + float top; + float bottom; + float right; + float radiusOffset = mRadius * 2; int topStart = getLocationOnScreen()[1]; switch (mDeltaRotation) { default: case Surface.ROTATION_0: case Surface.ROTATION_180: - int height = mRadius * 2; - left = getWidth() / 2 - mWidth / 2; + float height = mRadius * 2; + left = getWidth() / 2f - mWidth / 2f; top = (getHeight() - mBottom - height); - right = getWidth() / 2 + mWidth / 2; + right = getWidth() / 2f + mWidth / 2f; bottom = top + height; break; case Surface.ROTATION_90: left = mBottom; right = left + radiusOffset; - top = getHeight() / 2 - (mWidth / 2) - (topStart / 2); + top = getHeight() / 2f - (mWidth / 2f) - (topStart / 2f); bottom = top + mWidth; break; case Surface.ROTATION_270: right = getWidth() - mBottom; left = right - radiusOffset; - top = getHeight() / 2 - (mWidth / 2) - (topStart / 2); + top = getHeight() / 2f - (mWidth / 2f) - (topStart / 2f); bottom = top + mWidth; break; } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt index dbdb9d268aab..cd6eb99e259e 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -58,7 +58,8 @@ class PrivacyItemController @Inject constructor( internal companion object { val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA, AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO, - AppOpsManager.OP_PHONE_CALL_MICROPHONE) + AppOpsManager.OP_PHONE_CALL_MICROPHONE, + AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) val OPS_LOCATION = intArrayOf( AppOpsManager.OP_COARSE_LOCATION, AppOpsManager.OP_FINE_LOCATION) @@ -315,6 +316,7 @@ class PrivacyItemController @Inject constructor( AppOpsManager.OP_COARSE_LOCATION, AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION AppOpsManager.OP_PHONE_CALL_MICROPHONE, + AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE else -> return null } diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java index 8000bdccfa68..2c20feb19342 100644 --- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java +++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java @@ -29,7 +29,6 @@ import android.provider.Settings; import android.util.Log; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.settings.UserTracker; @@ -119,7 +118,6 @@ public class QRCodeScannerController implements mSecureSettings = secureSettings; mDeviceConfigProxy = proxy; mUserTracker = userTracker; - mConfigEnableLockScreenButton = mContext.getResources().getBoolean( android.R.bool.config_enableQrCodeScannerOnLockScreen); } @@ -258,16 +256,20 @@ public class QRCodeScannerController implements } } + private String getDefaultScannerActivity() { + return mContext.getResources().getString( + com.android.internal.R.string.config_defaultQrCodeComponent); + } + private void updateQRCodeScannerActivityDetails() { String qrCodeScannerActivity = mDeviceConfigProxy.getString( DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER, ""); // "" means either the flags is not available or is set to "", and in both the cases we - // want to use R.string.def_qr_code_component + // want to use R.string.config_defaultQrCodeComponent if (Objects.equals(qrCodeScannerActivity, "")) { - qrCodeScannerActivity = - mContext.getResources().getString(R.string.def_qr_code_component); + qrCodeScannerActivity = getDefaultScannerActivity(); } String prevQrCodeScannerActivity = mQRCodeScannerActivity; diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 3e8cdf3a3592..e5d7b4003297 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -23,10 +23,12 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.content.pm.UserInfo import android.graphics.drawable.Drawable import android.os.IBinder import android.os.PowerExemptionManager import android.os.RemoteException +import android.os.UserHandle import android.provider.DeviceConfig.NAMESPACE_SYSTEMUI import android.text.format.DateUtils import android.util.ArrayMap @@ -51,6 +53,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.DeviceConfigProxy import com.android.systemui.util.indentIfPossible @@ -69,6 +72,7 @@ class FgsManagerController @Inject constructor( private val systemClock: SystemClock, private val activityManager: IActivityManager, private val packageManager: PackageManager, + private val userTracker: UserTracker, private val deviceConfigProxy: DeviceConfigProxy, private val dialogLaunchAnimator: DialogLaunchAnimator, private val broadcastDispatcher: BroadcastDispatcher, @@ -82,7 +86,8 @@ class FgsManagerController @Inject constructor( var changesSinceDialog = false private set - private var isAvailable = false + var isAvailable = false + private set private val lock = Any() @@ -90,6 +95,12 @@ class FgsManagerController @Inject constructor( var initialized = false @GuardedBy("lock") + private var lastNumberOfVisiblePackages = 0 + + @GuardedBy("lock") + private var currentProfileIds = mutableSetOf<Int>() + + @GuardedBy("lock") private val runningServiceTokens = mutableMapOf<UserPackage, StartTimeAndTokens>() @GuardedBy("lock") @@ -101,6 +112,19 @@ class FgsManagerController @Inject constructor( @GuardedBy("lock") private var runningApps: ArrayMap<UserPackage, RunningApp> = ArrayMap() + private val userTrackerCallback = object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) {} + + override fun onProfilesChanged(profiles: List<UserInfo>) { + synchronized(lock) { + currentProfileIds.clear() + currentProfileIds.addAll(profiles.map { it.id }) + lastNumberOfVisiblePackages = 0 + updateNumberOfVisibleRunningPackagesLocked() + } + } + } + interface OnNumberOfPackagesChangedListener { fun onNumberOfPackagesChanged(numPackages: Int) } @@ -120,6 +144,10 @@ class FgsManagerController @Inject constructor( e.rethrowFromSystemServer() } + userTracker.addCallback(userTrackerCallback, backgroundExecutor) + + currentProfileIds.addAll(userTracker.userProfiles.map { it.id }) + deviceConfigProxy.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI, backgroundExecutor) { isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable) @@ -153,10 +181,9 @@ class FgsManagerController @Inject constructor( isForeground: Boolean ) { synchronized(lock) { - val numPackagesBefore = getNumRunningPackagesLocked() val userPackageKey = UserPackage(userId, packageName) if (isForeground) { - runningServiceTokens.getOrPut(userPackageKey, { StartTimeAndTokens(systemClock) }) + runningServiceTokens.getOrPut(userPackageKey) { StartTimeAndTokens(systemClock) } .addToken(token) } else { if (runningServiceTokens[userPackageKey]?.also { @@ -165,14 +192,7 @@ class FgsManagerController @Inject constructor( } } - val numPackagesAfter = getNumRunningPackagesLocked() - - if (numPackagesAfter != numPackagesBefore) { - changesSinceDialog = true - onNumberOfPackagesChangedListeners.forEach { - backgroundExecutor.execute { it.onNumberOfPackagesChanged(numPackagesAfter) } - } - } + updateNumberOfVisibleRunningPackagesLocked() updateAppItemsLocked() } @@ -209,18 +229,30 @@ class FgsManagerController @Inject constructor( } } - fun isAvailable(): Boolean { - return isAvailable - } - fun getNumRunningPackages(): Int { synchronized(lock) { - return getNumRunningPackagesLocked() + return getNumVisiblePackagesLocked() } } - private fun getNumRunningPackagesLocked() = - runningServiceTokens.keys.count { it.uiControl != UIControl.HIDE_ENTRY } + private fun getNumVisiblePackagesLocked(): Int { + return runningServiceTokens.keys.count { + it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId) + } + } + + private fun updateNumberOfVisibleRunningPackagesLocked() { + val num = getNumVisiblePackagesLocked() + if (num != lastNumberOfVisiblePackages) { + lastNumberOfVisiblePackages = num + changesSinceDialog = true + onNumberOfPackagesChangedListeners.forEach { + backgroundExecutor.execute { + it.onNumberOfPackagesChanged(num) + } + } + } + } fun shouldUpdateFooterVisibility() = dialog == null @@ -289,7 +321,9 @@ class FgsManagerController @Inject constructor( val ai = packageManager.getApplicationInfoAsUser(it.packageName, 0, it.userId) runningApps[it] = RunningApp(it.userId, it.packageName, runningServiceTokens[it]!!.startTime, it.uiControl, - ai.loadLabel(packageManager), ai.loadIcon(packageManager)) + packageManager.getApplicationLabel(ai), + packageManager.getUserBadgedIcon( + packageManager.getApplicationIcon(ai), UserHandle.of(it.userId))) logEvent(stopped = false, it.packageName, it.userId, runningApps[it]!!.timeStarted) } @@ -404,6 +438,7 @@ class FgsManagerController @Inject constructor( val packageName: String ) { val uid by lazy { packageManager.getPackageUidAsUser(packageName, userId) } + var backgroundRestrictionExemptionReason = PowerExemptionManager.REASON_DENIED private var uiControlInitialized = false var uiControl: UIControl = UIControl.NORMAL @@ -416,7 +451,9 @@ class FgsManagerController @Inject constructor( private set fun updateUiControl() { - uiControl = when (activityManager.getBackgroundRestrictionExemptionReason(uid)) { + backgroundRestrictionExemptionReason = + activityManager.getBackgroundRestrictionExemptionReason(uid) + uiControl = when (backgroundRestrictionExemptionReason) { PowerExemptionManager.REASON_SYSTEM_UID, PowerExemptionManager.REASON_DEVICE_DEMO_MODE -> UIControl.HIDE_ENTRY @@ -448,7 +485,7 @@ class FgsManagerController @Inject constructor( pw.indentIfPossible { pw.println("userId=$userId") pw.println("packageName=$packageName") - pw.println("uiControl=$uiControl") + pw.println("uiControl=$uiControl (reason=$backgroundRestrictionExemptionReason)") } pw.println("]") } @@ -525,7 +562,7 @@ class FgsManagerController @Inject constructor( pw.println("userId=$userId") pw.println("packageName=$packageName") pw.println("timeStarted=$timeStarted (time since start =" + - " ${systemClock.elapsedRealtime() - timeStarted}ms)\"") + " ${systemClock.elapsedRealtime() - timeStarted}ms)") pw.println("uiControl=$uiControl") pw.println("appLabel=$appLabel") pw.println("icon=$icon") @@ -542,6 +579,7 @@ class FgsManagerController @Inject constructor( override fun dump(printwriter: PrintWriter, args: Array<out String>) { val pw = IndentingPrintWriter(printwriter) synchronized(lock) { + pw.println("current user profiles = $currentProfileIds") pw.println("changesSinceDialog=$changesSinceDialog") pw.println("Running service tokens: [") pw.indentIfPossible { @@ -560,8 +598,10 @@ class FgsManagerController @Inject constructor( pw.indentIfPossible { runningApps.forEach { (userPackage, runningApp) -> pw.println("{") - userPackage.dump(pw) - runningApp.dump(pw, systemClock) + pw.indentIfPossible { + userPackage.dump(pw) + runningApp.dump(pw, systemClock) + } pw.println("}") } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java index dd99db49c1d2..584de6e8c28f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java @@ -22,6 +22,9 @@ import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NETWORK; import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TITLE; import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_CA_CERT_SUBTITLE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_NETWORK_SUBTITLE; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_VPN_SUBTITLE; import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_NAMED_MANAGEMENT; import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN; import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_VIEW_POLICIES; @@ -92,6 +95,7 @@ import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.util.ViewController; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import javax.inject.Inject; import javax.inject.Named; @@ -106,7 +110,7 @@ class QSSecurityFooter extends ViewController<View> private final TextView mFooterText; private final ImageView mPrimaryFooterIcon; - private final Context mContext; + private Context mContext; private final DevicePolicyManager mDpm; private final Callback mCallback = new Callback(); private final SecurityController mSecurityController; @@ -141,6 +145,63 @@ class QSSecurityFooter extends ViewController<View> } }; + private Supplier<String> mManagementTitleSupplier = () -> + mContext == null ? null : mContext.getString(R.string.monitoring_title_device_owned); + + private Supplier<String> mManagementMessageSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_management); + + private Supplier<String> mManagementMonitoringStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_management_monitoring); + + private Supplier<String> mManagementMultipleVpnStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_management_vpns); + + private Supplier<String> mWorkProfileMonitoringStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_managed_profile_monitoring); + + private Supplier<String> mWorkProfileNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.quick_settings_disclosure_managed_profile_network_activity); + + private Supplier<String> mMonitoringSubtitleCaCertStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_subtitle_ca_certificate); + + private Supplier<String> mMonitoringSubtitleNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_subtitle_network_logging); + + private Supplier<String> mMonitoringSubtitleVpnStringSupplier = () -> + mContext == null ? null : mContext.getString(R.string.monitoring_subtitle_vpn); + + private Supplier<String> mViewPoliciesButtonStringSupplier = () -> + mContext == null ? null : mContext.getString(R.string.monitoring_button_view_policies); + + private Supplier<String> mManagementDialogStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_management); + + private Supplier<String> mManagementDialogCaCertStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_management_ca_certificate); + + private Supplier<String> mWorkProfileDialogCaCertStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_managed_profile_ca_certificate); + + private Supplier<String> mManagementDialogNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_management_network_logging); + + private Supplier<String> mWorkProfileDialogNetworkStringSupplier = () -> + mContext == null ? null : mContext.getString( + R.string.monitoring_description_managed_profile_network_logging); + @Inject QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView, UserTracker userTracker, @Main Handler mainHandler, @@ -337,9 +398,7 @@ class QSSecurityFooter extends ViewController<View> private String getManagedDeviceMonitoringText(CharSequence organizationName) { if (organizationName == null) { return mDpm.getResources().getString( - QS_MSG_MANAGEMENT_MONITORING, - () -> mContext.getString( - R.string.quick_settings_disclosure_management_monitoring)); + QS_MSG_MANAGEMENT_MONITORING, mManagementMonitoringStringSupplier); } return mDpm.getResources().getString( QS_MSG_NAMED_MANAGEMENT_MONITORING, @@ -354,9 +413,7 @@ class QSSecurityFooter extends ViewController<View> if (vpnName != null && vpnNameWorkProfile != null) { if (organizationName == null) { return mDpm.getResources().getString( - QS_MSG_MANAGEMENT_MULTIPLE_VPNS, - () -> mContext.getString( - R.string.quick_settings_disclosure_management_vpns)); + QS_MSG_MANAGEMENT_MULTIPLE_VPNS, mManagementMultipleVpnStringSupplier); } return mDpm.getResources().getString( QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS, @@ -386,10 +443,7 @@ class QSSecurityFooter extends ViewController<View> private String getMangedDeviceGeneralText(CharSequence organizationName) { if (organizationName == null) { - return mDpm.getResources().getString( - QS_MSG_MANAGEMENT, - () -> mContext.getString( - R.string.quick_settings_disclosure_management)); + return mDpm.getResources().getString(QS_MSG_MANAGEMENT, mManagementMessageSupplier); } if (isFinancedDevice()) { return mContext.getString( @@ -431,9 +485,7 @@ class QSSecurityFooter extends ViewController<View> if (hasCACertsInWorkProfile && isWorkProfileOn) { if (workProfileOrganizationName == null) { return mDpm.getResources().getString( - QS_MSG_WORK_PROFILE_MONITORING, - () -> mContext.getString( - R.string.quick_settings_disclosure_managed_profile_monitoring)); + QS_MSG_WORK_PROFILE_MONITORING, mWorkProfileMonitoringStringSupplier); } return mDpm.getResources().getString( QS_MSG_NAMED_WORK_PROFILE_MONITORING, @@ -478,10 +530,9 @@ class QSSecurityFooter extends ViewController<View> private String getManagedProfileNetworkActivityText() { return mDpm.getResources().getString( - QS_MSG_WORK_PROFILE_NETWORK, - () -> mContext.getString( - R.string.quick_settings_disclosure_managed_profile_network_activity)); + QS_MSG_WORK_PROFILE_NETWORK, mWorkProfileNetworkStringSupplier); } + @Override public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_NEGATIVE) { @@ -569,6 +620,12 @@ class QSSecurityFooter extends ViewController<View> caCertsWarning.setText(caCertsMessage); // Make "Open trusted credentials"-link clickable caCertsWarning.setMovementMethod(new LinkMovementMethod()); + + TextView caCertsSubtitle = (TextView) dialogView.findViewById(R.id.ca_certs_subtitle); + String caCertsSubtitleMessage = mDpm.getResources().getString( + QS_DIALOG_MONITORING_CA_CERT_SUBTITLE, mMonitoringSubtitleCaCertStringSupplier); + caCertsSubtitle.setText(caCertsSubtitleMessage); + } // network logging section @@ -581,6 +638,13 @@ class QSSecurityFooter extends ViewController<View> TextView networkLoggingWarning = (TextView) dialogView.findViewById(R.id.network_logging_warning); networkLoggingWarning.setText(networkLoggingMessage); + + TextView networkLoggingSubtitle = (TextView) dialogView.findViewById( + R.id.network_logging_subtitle); + String networkLoggingSubtitleMessage = mDpm.getResources().getString( + QS_DIALOG_MONITORING_NETWORK_SUBTITLE, + mMonitoringSubtitleNetworkStringSupplier); + networkLoggingSubtitle.setText(networkLoggingSubtitleMessage); } // vpn section @@ -594,6 +658,11 @@ class QSSecurityFooter extends ViewController<View> vpnWarning.setText(vpnMessage); // Make "Open VPN Settings"-link clickable vpnWarning.setMovementMethod(new LinkMovementMethod()); + + TextView vpnSubtitle = (TextView) dialogView.findViewById(R.id.vpn_subtitle); + String vpnSubtitleMessage = mDpm.getResources().getString( + QS_DIALOG_MONITORING_VPN_SUBTITLE, mMonitoringSubtitleVpnStringSupplier); + vpnSubtitle.setText(vpnSubtitleMessage); } // Note: if a new section is added, should update configSubtitleVisibility to include @@ -657,8 +726,7 @@ class QSSecurityFooter extends ViewController<View> @VisibleForTesting String getSettingsButton() { return mDpm.getResources().getString( - QS_DIALOG_VIEW_POLICIES, - () -> mContext.getString(R.string.monitoring_button_view_policies)); + QS_DIALOG_VIEW_POLICIES, mViewPoliciesButtonStringSupplier); } private String getPositiveButton() { @@ -692,9 +760,7 @@ class QSSecurityFooter extends ViewController<View> organizationName); } } - return mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT, - () -> mContext.getString(R.string.monitoring_description_management)); + return mDpm.getResources().getString(QS_DIALOG_MANAGEMENT, mManagementDialogStringSupplier); } @Nullable @@ -703,15 +769,11 @@ class QSSecurityFooter extends ViewController<View> if (!(hasCACerts || hasCACertsInWorkProfile)) return null; if (isDeviceManaged) { return mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_CA_CERT, - () -> mContext.getString( - R.string.monitoring_description_management_ca_certificate)); + QS_DIALOG_MANAGEMENT_CA_CERT, mManagementDialogCaCertStringSupplier); } if (hasCACertsInWorkProfile) { return mDpm.getResources().getString( - QS_DIALOG_WORK_PROFILE_CA_CERT, - () -> mContext.getString( - R.string.monitoring_description_managed_profile_ca_certificate)); + QS_DIALOG_WORK_PROFILE_CA_CERT, mWorkProfileDialogCaCertStringSupplier); } return mContext.getString(R.string.monitoring_description_ca_certificate); } @@ -722,14 +784,10 @@ class QSSecurityFooter extends ViewController<View> if (!isNetworkLoggingEnabled) return null; if (isDeviceManaged) { return mDpm.getResources().getString( - QS_DIALOG_MANAGEMENT_NETWORK, - () -> mContext.getString( - R.string.monitoring_description_management_network_logging)); + QS_DIALOG_MANAGEMENT_NETWORK, mManagementDialogNetworkStringSupplier); } else { return mDpm.getResources().getString( - QS_DIALOG_WORK_PROFILE_NETWORK, - () -> mContext.getString( - R.string.monitoring_description_managed_profile_network_logging)); + QS_DIALOG_WORK_PROFILE_NETWORK, mWorkProfileDialogNetworkStringSupplier); } } @@ -799,7 +857,7 @@ class QSSecurityFooter extends ViewController<View> } else { return mDpm.getResources().getString( QS_DIALOG_MANAGEMENT_TITLE, - () -> mContext.getString(R.string.monitoring_title_device_owned)); + mManagementTitleSupplier); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java index d7aa8b21a150..7c8e77b5d993 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java @@ -30,6 +30,7 @@ import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -158,7 +159,9 @@ public class HotspotTile extends QSTileImpl<BooleanState> { state.expandedAccessibilityClassName = Switch.class.getName(); state.contentDescription = state.label; - final boolean isTileUnavailable = isDataSaverEnabled; + final boolean isWifiTetheringAllowed = + WifiEnterpriseRestrictionUtils.isWifiTetheringAllowed(mHost.getUserContext()); + final boolean isTileUnavailable = isDataSaverEnabled || !isWifiTetheringAllowed; final boolean isTileActive = (state.value || state.isTransient); if (isTileUnavailable) { @@ -167,15 +170,17 @@ public class HotspotTile extends QSTileImpl<BooleanState> { state.state = isTileActive ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; } - state.secondaryLabel = getSecondaryLabel( - isTileActive, isTransient, isDataSaverEnabled, numConnectedDevices); + state.secondaryLabel = getSecondaryLabel(isTileActive, isTransient, isDataSaverEnabled, + numConnectedDevices, isWifiTetheringAllowed); state.stateDescription = state.secondaryLabel; } @Nullable private String getSecondaryLabel(boolean isActive, boolean isTransient, - boolean isDataSaverEnabled, int numConnectedDevices) { - if (isTransient) { + boolean isDataSaverEnabled, int numConnectedDevices, boolean isWifiTetheringAllowed) { + if (!isWifiTetheringAllowed) { + return mContext.getString(R.string.wifitrackerlib_admin_restricted_network); + } else if (isTransient) { return mContext.getString(R.string.quick_settings_hotspot_secondary_label_transient); } else if (isDataSaverEnabled) { return mContext.getString( diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 6d9455e80adc..489f881c36cf 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -78,7 +78,6 @@ import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManagerGlobal; -import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.Toast; import android.window.WindowContext; @@ -559,14 +558,9 @@ public class ScreenshotController { private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, ComponentName topComponent, boolean showFlash) { - if (mAccessibilityManager.isEnabled()) { - AccessibilityEvent event = - new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - event.setContentDescription( - mContext.getResources().getString(R.string.screenshot_saving_title)); - mAccessibilityManager.sendAccessibilityEvent(event); - } - + withWindowAttached(() -> + mScreenshotView.announceForAccessibility( + mContext.getResources().getString(R.string.screenshot_saving_title))); if (mScreenshotView.isAttachedToWindow()) { // if we didn't already dismiss for another reason @@ -633,6 +627,7 @@ public class ScreenshotController { } } } + @Override public void requestCompatCameraControl(boolean showControl, boolean transformationApplied, @@ -718,6 +713,7 @@ public class ScreenshotController { Log.e(TAG, "requestScrollCapture failed", e); } } + ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture; private void runBatchScrollCapture(ScrollCaptureResponse response) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 48bb2af2ab8d..79939c87173b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -820,7 +820,7 @@ public class ScreenshotView extends FrameLayout implements animateDismissal(); }); actionChip.setAlpha(1); - mActionsView.addView(actionChip); + mActionsView.addView(actionChip, mActionsView.getChildCount() - 1); mSmartChips.add(actionChip); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 7f3758e208db..2621f6da7afa 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -16,6 +16,7 @@ package com.android.systemui.screenshot; +import static android.app.admin.DevicePolicyResources.Strings.SystemUi.SCREENSHOT_BLOCKED_BY_ADMIN; import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE; @@ -54,7 +55,9 @@ import androidx.annotation.NonNull; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.ScreenshotHelper; import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Background; +import java.util.concurrent.Executor; import java.util.function.Consumer; import javax.inject.Inject; @@ -70,6 +73,7 @@ public class TakeScreenshotService extends Service { private final ScreenshotNotificationsController mNotificationsController; private final Handler mHandler; private final Context mContext; + private final @Background Executor mBgExecutor; private final BroadcastReceiver mCloseSystemDialogs = new BroadcastReceiver() { @Override @@ -97,7 +101,8 @@ public class TakeScreenshotService extends Service { @Inject public TakeScreenshotService(ScreenshotController screenshotController, UserManager userManager, DevicePolicyManager devicePolicyManager, UiEventLogger uiEventLogger, - ScreenshotNotificationsController notificationsController, Context context) { + ScreenshotNotificationsController notificationsController, Context context, + @Background Executor bgExecutor) { if (DEBUG_SERVICE) { Log.d(TAG, "new " + this); } @@ -108,6 +113,7 @@ public class TakeScreenshotService extends Service { mUiEventLogger = uiEventLogger; mNotificationsController = notificationsController; mContext = context; + mBgExecutor = bgExecutor; } @Override @@ -189,12 +195,18 @@ public class TakeScreenshotService extends Service { requestCallback.reportError(); return true; } - if(mDevicePolicyManager.getScreenCaptureDisabled(null, UserHandle.USER_ALL)) { - Log.w(TAG, "Skipping screenshot because an IT admin has disabled " - + "screenshots on the device"); - Toast.makeText(mContext, R.string.screenshot_blocked_by_admin, - Toast.LENGTH_SHORT).show(); - requestCallback.reportError(); + + if (mDevicePolicyManager.getScreenCaptureDisabled(null, UserHandle.USER_ALL)) { + mBgExecutor.execute(() -> { + Log.w(TAG, "Skipping screenshot because an IT admin has disabled " + + "screenshots on the device"); + String blockedByAdminText = mDevicePolicyManager.getResources().getString( + SCREENSHOT_BLOCKED_BY_ADMIN, + () -> mContext.getString(R.string.screenshot_blocked_by_admin)); + mHandler.post(() -> + Toast.makeText(mContext, blockedByAdminText, Toast.LENGTH_SHORT).show()); + requestCallback.reportError(); + }); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java index 7530681c82b2..f68e0429ef7c 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java @@ -58,6 +58,7 @@ public class ScrimView extends View { private Drawable mDrawable; private PorterDuffColorFilter mColorFilter; private int mTintColor; + private boolean mBlendWithMainColor = true; private Runnable mChangeRunnable; private Executor mChangeRunnableExecutor; private Executor mExecutor; @@ -192,6 +193,19 @@ public class ScrimView extends View { } /** + * The call to {@link #setTint} will blend with the main color, with the amount + * determined by the alpha of the tint. Set to false to avoid this blend. + */ + public void setBlendWithMainColor(boolean blend) { + mBlendWithMainColor = blend; + } + + /** @return true if blending tint color with main color */ + public boolean shouldBlendWithMainColor() { + return mBlendWithMainColor; + } + + /** * Tints this view, optionally animating it. * @param color The color. * @param animated If we should animate. @@ -211,8 +225,11 @@ public class ScrimView extends View { // Optimization to blend colors and avoid a color filter ScrimDrawable drawable = (ScrimDrawable) mDrawable; float tintAmount = Color.alpha(mTintColor) / 255f; - int mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor, - tintAmount); + + int mainTinted = mTintColor; + if (mBlendWithMainColor) { + mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor, tintAmount); + } drawable.setColor(mainTinted, animated); } else { boolean hasAlpha = Color.alpha(mTintColor) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java index 4d933d9ad21e..e44d334c776d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java @@ -29,7 +29,6 @@ import com.android.keyguard.AlphaOptimizedLinearLayout; import com.android.systemui.R; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.NotificationEntry.OnSensitivityChangedListener; import java.util.ArrayList; @@ -49,6 +48,7 @@ public class HeadsUpStatusBarView extends AlphaOptimizedLinearLayout { private TextView mTextView; private NotificationEntry mShowingEntry; private Runnable mOnDrawingRectChangedListener; + private boolean mRedactSensitiveContent; public HeadsUpStatusBarView(Context context) { this(context, null); @@ -111,29 +111,28 @@ public class HeadsUpStatusBarView extends AlphaOptimizedLinearLayout { } public void setEntry(NotificationEntry entry) { - if (mShowingEntry != null) { - mShowingEntry.removeOnSensitivityChangedListener(mOnSensitivityChangedListener); - } mShowingEntry = entry; - if (mShowingEntry != null) { CharSequence text = entry.headsUpStatusBarText; - if (entry.isSensitive()) { + if (mRedactSensitiveContent && entry.hasSensitiveContents()) { text = entry.headsUpStatusBarTextPublic; } mTextView.setText(text); - mShowingEntry.addOnSensitivityChangedListener(mOnSensitivityChangedListener); } } - private final OnSensitivityChangedListener mOnSensitivityChangedListener = entry -> { - if (entry != mShowingEntry) { - throw new IllegalStateException("Got a sensitivity change for " + entry - + " but mShowingEntry is " + mShowingEntry); + public void setRedactSensitiveContent(boolean redactSensitiveContent) { + if (mRedactSensitiveContent == redactSensitiveContent) { + return; + } + mRedactSensitiveContent = redactSensitiveContent; + if (mShowingEntry != null && mShowingEntry.hasSensitiveContents()) { + mTextView.setText( + mRedactSensitiveContent + ? mShowingEntry.headsUpStatusBarTextPublic + : mShowingEntry.headsUpStatusBarText); } - // Update the text - setEntry(entry); - }; + } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index c1ea6bf7cec8..cbe722b6f82f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -386,7 +386,7 @@ class LockscreenShadeTransitionController @Inject constructor( } if (view is ExpandableNotificationRow) { // Only drag down on sensitive views, otherwise the ExpandHelper will take this - return view.entry.isSensitive + return lockScreenUserManager.notifNeedsRedactionInPublic(view.entry) } } return false @@ -552,7 +552,8 @@ class LockscreenShadeTransitionController @Inject constructor( logger.logShadeDisabledOnGoToLockedShade() return } - var userId: Int = lockScreenUserManager.getCurrentUserId() + val currentUser = lockScreenUserManager.currentUserId + var userId: Int = currentUser var entry: NotificationEntry? = null if (expandView is ExpandableNotificationRow) { entry = expandView.entry @@ -562,12 +563,18 @@ class LockscreenShadeTransitionController @Inject constructor( entry.setGroupExpansionChanging(true) userId = entry.sbn.userId } - var fullShadeNeedsBouncer = (!lockScreenUserManager.userAllowsPrivateNotificationsInPublic( - lockScreenUserManager.getCurrentUserId()) || - !lockScreenUserManager.shouldShowLockscreenNotifications() || - falsingCollector.shouldEnforceBouncer()) - if (keyguardBypassController.bypassEnabled) { - fullShadeNeedsBouncer = false + val fullShadeNeedsBouncer = when { + // No bouncer necessary if we're bypassing + keyguardBypassController.bypassEnabled -> false + // Redacted notificationss are present, bouncer should be shown before un-redacting in + // the full shade + lockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(currentUser) -> true + // Notifications are hidden in public, bouncer should be shown before showing them in + // the full shade + !lockScreenUserManager.shouldShowLockscreenNotifications() -> true + // Bouncer is being enforced, so we need to show it + falsingCollector.shouldEnforceBouncer() -> true + else -> false } if (lockScreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) { statusBarStateController.setLeaveOpenOnKeyguardHide(true) @@ -911,4 +918,4 @@ class DragDownHelper( host.getLocationOnScreen(temp2) return expandCallback.getChildAtRawPosition(x + temp2[0], y + temp2[1]) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java index 9a1a144924e2..5fd9671bda01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java @@ -71,17 +71,22 @@ public interface NotificationLockscreenUserManager { boolean shouldHideNotifications(String key); boolean shouldShowOnKeyguard(NotificationEntry entry); + void addOnNeedsRedactionInPublicChangedListener(Runnable listener); + + void removeOnNeedsRedactionInPublicChangedListener(Runnable listener); + boolean isAnyProfilePublicMode(); void updatePublicMode(); - boolean needsRedaction(NotificationEntry entry); + /** Does this notification require redaction if it is displayed when the device is public? */ + boolean notifNeedsRedactionInPublic(NotificationEntry entry); /** - * Has the given user chosen to allow their private (full) notifications to be shown even - * when the lockscreen is in "public" (secure & locked) mode? + * Do all sensitive notifications belonging to the given user require redaction when they are + * displayed in public? */ - boolean userAllowsPrivateNotificationsInPublic(int currentUserId); + boolean sensitiveNotifsNeedRedactionInPublic(int userId); /** * Has the given user chosen to allow notifications to be shown even when the lockscreen is in diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 56e09f03aa6b..334cfe5f4c41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -24,7 +24,6 @@ import static com.android.systemui.statusbar.notification.stack.NotificationPrio import android.app.ActivityManager; import android.app.KeyguardManager; -import android.app.Notification; import android.app.NotificationManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -45,7 +44,6 @@ import android.util.SparseBooleanArray; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -60,6 +58,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Co import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.ListenerSet; import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; @@ -85,13 +84,12 @@ public class NotificationLockscreenUserManagerImpl implements private final DeviceProvisionedController mDeviceProvisionedController; private final KeyguardStateController mKeyguardStateController; private final SecureSettings mSecureSettings; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final Lazy<OverviewProxyService> mOverviewProxyService; private final Object mLock = new Object(); - - // Lazy - private NotificationEntryManager mEntryManager; - private final Lazy<NotificationVisibilityProvider> mVisibilityProviderLazy; private final Lazy<CommonNotifCollection> mCommonNotifCollectionLazy; + private final Lazy<NotificationEntryManager> mEntryManagerLazy; private final DevicePolicyManager mDevicePolicyManager; private final SparseBooleanArray mLockscreenPublicMode = new SparseBooleanArray(); private final SparseBooleanArray mUsersWithSeparateWorkChallenge = new SparseBooleanArray(); @@ -103,13 +101,14 @@ public class NotificationLockscreenUserManagerImpl implements private final List<UserChangedListener> mListeners = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; private final NotificationClickNotifier mClickNotifier; - - private boolean mShowLockscreenNotifications; - private boolean mAllowLockscreenRemoteInput; - private LockPatternUtils mLockPatternUtils; - protected KeyguardManager mKeyguardManager; - private int mState = StatusBarState.SHADE; - private List<KeyguardNotificationSuppressor> mKeyguardSuppressors = new ArrayList<>(); + private final LockPatternUtils mLockPatternUtils; + private final List<KeyguardNotificationSuppressor> mKeyguardSuppressors = new ArrayList<>(); + protected final Context mContext; + private final Handler mMainHandler; + protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>(); + protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>(); + private final ListenerSet<Runnable> mOnSensitiveContentRedactionChangeListeners = + new ListenerSet<>(); protected final BroadcastReceiver mAllUsersReceiver = new BroadcastReceiver() { @Override @@ -120,7 +119,11 @@ public class NotificationLockscreenUserManagerImpl implements isCurrentProfile(getSendingUserId())) { mUsersAllowingPrivateNotifications.clear(); updateLockscreenNotificationSetting(); - getEntryManager().updateNotifications("ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED"); + for (Runnable listener : mOnSensitiveContentRedactionChangeListeners) { + listener.run(); + } + mEntryManagerLazy.get() + .updateNotifications("ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED"); } } }; @@ -142,7 +145,7 @@ public class NotificationLockscreenUserManagerImpl implements // The filtering needs to happen before the update call below in order to // make sure // the presenter has the updated notifications from the new user - getEntryManager().reapplyFilterAndSort("user switched"); + mEntryManagerLazy.get().reapplyFilterAndSort("user switched"); mPresenter.onUserSwitched(mCurrentUserId); for (UserChangedListener listener : mListeners) { @@ -156,7 +159,7 @@ public class NotificationLockscreenUserManagerImpl implements break; case Intent.ACTION_USER_UNLOCKED: // Start the overview connection to the launcher service - Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser(); + mOverviewProxyService.get().startConnectionToCurrentUser(); break; case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION: final IntentSender intentSender = intent.getParcelableExtra( @@ -179,28 +182,26 @@ public class NotificationLockscreenUserManagerImpl implements } }; - protected final Context mContext; - private final Handler mMainHandler; - protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>(); - protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>(); - - protected int mCurrentUserId = 0; + // Late-init protected NotificationPresenter mPresenter; protected ContentObserver mLockscreenSettingsObserver; protected ContentObserver mSettingsObserver; - private boolean mHideSilentNotificationsOnLockscreen; + protected KeyguardManager mKeyguardManager; - private NotificationEntryManager getEntryManager() { - if (mEntryManager == null) { - mEntryManager = Dependency.get(NotificationEntryManager.class); - } - return mEntryManager; - } + protected int mCurrentUserId = 0; + private int mState = StatusBarState.SHADE; + private boolean mHideSilentNotificationsOnLockscreen; + private boolean mShowLockscreenNotifications; + private boolean mAllowLockscreenRemoteInput; @Inject - public NotificationLockscreenUserManagerImpl(Context context, + public NotificationLockscreenUserManagerImpl( + Context context, BroadcastDispatcher broadcastDispatcher, DevicePolicyManager devicePolicyManager, + KeyguardUpdateMonitor keyguardUpdateMonitor, + Lazy<NotificationEntryManager> notificationEntryManagerLazy, + Lazy<OverviewProxyService> overviewProxyServiceLazy, UserManager userManager, Lazy<NotificationVisibilityProvider> visibilityProviderLazy, Lazy<CommonNotifCollection> commonNotifCollectionLazy, @@ -216,9 +217,11 @@ public class NotificationLockscreenUserManagerImpl implements mMainHandler = mainHandler; mDevicePolicyManager = devicePolicyManager; mUserManager = userManager; + mOverviewProxyService = overviewProxyServiceLazy; mCurrentUserId = ActivityManager.getCurrentUser(); mVisibilityProviderLazy = visibilityProviderLazy; mCommonNotifCollectionLazy = commonNotifCollectionLazy; + mEntryManagerLazy = notificationEntryManagerLazy; mClickNotifier = clickNotifier; statusBarStateController.addCallback(this); mLockPatternUtils = new LockPatternUtils(context); @@ -227,10 +230,12 @@ public class NotificationLockscreenUserManagerImpl implements mDeviceProvisionedController = deviceProvisionedController; mSecureSettings = secureSettings; mKeyguardStateController = keyguardStateController; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; dumpManager.registerDumpable(this); } + @Override public void setUpWithPresenter(NotificationPresenter presenter) { mPresenter = presenter; @@ -243,7 +248,10 @@ public class NotificationLockscreenUserManagerImpl implements mUsersAllowingNotifications.clear(); // ... and refresh all the notifications updateLockscreenNotificationSetting(); - getEntryManager().updateNotifications("LOCK_SCREEN_SHOW_NOTIFICATIONS," + for (Runnable listener : mOnSensitiveContentRedactionChangeListeners) { + listener.run(); + } + mEntryManagerLazy.get().updateNotifications("LOCK_SCREEN_SHOW_NOTIFICATIONS," + " or LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS change"); } }; @@ -253,7 +261,7 @@ public class NotificationLockscreenUserManagerImpl implements public void onChange(boolean selfChange) { updateLockscreenNotificationSetting(); if (mDeviceProvisionedController.isDeviceProvisioned()) { - getEntryManager().updateNotifications("LOCK_SCREEN_ALLOW_REMOTE_INPUT" + mEntryManagerLazy.get().updateNotifications("LOCK_SCREEN_ALLOW_REMOTE_INPUT" + " or ZEN_MODE change"); } } @@ -312,14 +320,17 @@ public class NotificationLockscreenUserManagerImpl implements mSettingsObserver.onChange(false); // set up } + @Override public boolean shouldShowLockscreenNotifications() { return mShowLockscreenNotifications; } + @Override public boolean shouldAllowLockscreenRemoteInput() { return mAllowLockscreenRemoteInput; } + @Override public boolean isCurrentProfile(int userId) { synchronized (mLock) { return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null; @@ -334,7 +345,7 @@ public class NotificationLockscreenUserManagerImpl implements if (userId == UserHandle.USER_ALL) { userId = mCurrentUserId; } - boolean inLockdown = Dependency.get(KeyguardUpdateMonitor.class).isUserInLockdown(userId); + boolean inLockdown = mKeyguardUpdateMonitor.isUserInLockdown(userId); mUsersInLockdownLatestResult.put(userId, inLockdown); return inLockdown; } @@ -343,6 +354,7 @@ public class NotificationLockscreenUserManagerImpl implements * Returns true if we're on a secure lockscreen and the user wants to hide notification data. * If so, notifications should be hidden. */ + @Override public boolean shouldHideNotifications(int userId) { boolean hide = isLockscreenPublicMode(userId) && !userAllowsNotificationsInPublic(userId) || (userId != mCurrentUserId && shouldHideNotifications(mCurrentUserId)) @@ -355,6 +367,7 @@ public class NotificationLockscreenUserManagerImpl implements * Returns true if we're on a secure lockscreen and the user wants to hide notifications via * package-specific override. */ + @Override public boolean shouldHideNotifications(String key) { if (mCommonNotifCollectionLazy.get() == null) { Log.wtf(TAG, "mCommonNotifCollectionLazy was null!", new Throwable()); @@ -365,6 +378,7 @@ public class NotificationLockscreenUserManagerImpl implements && visibleEntry.getRanking().getLockscreenVisibilityOverride() == VISIBILITY_SECRET; } + @Override public boolean shouldShowOnKeyguard(NotificationEntry entry) { if (mCommonNotifCollectionLazy.get() == null) { Log.wtf(TAG, "mCommonNotifCollectionLazy was null!", new Throwable()); @@ -387,14 +401,6 @@ public class NotificationLockscreenUserManagerImpl implements return mShowLockscreenNotifications && exceedsPriorityThreshold; } - private void setShowLockscreenNotifications(boolean show) { - mShowLockscreenNotifications = show; - } - - private void setLockscreenAllowRemoteInput(boolean allowLockscreenRemoteInput) { - mAllowLockscreenRemoteInput = allowLockscreenRemoteInput; - } - protected void updateLockscreenNotificationSetting() { final boolean show = mSecureSettings.getIntForUser( Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, @@ -408,7 +414,7 @@ public class NotificationLockscreenUserManagerImpl implements mHideSilentNotificationsOnLockscreen = mSecureSettings.getIntForUser( Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1, mCurrentUserId) == 0; - setShowLockscreenNotifications(show && allowedByDpm); + mShowLockscreenNotifications = show && allowedByDpm; if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) { final boolean remoteInput = mSecureSettings.getIntForUser( @@ -418,9 +424,9 @@ public class NotificationLockscreenUserManagerImpl implements final boolean remoteInputDpm = (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT) == 0; - setLockscreenAllowRemoteInput(remoteInput && remoteInputDpm); + mAllowLockscreenRemoteInput = remoteInput && remoteInputDpm; } else { - setLockscreenAllowRemoteInput(false); + mAllowLockscreenRemoteInput = false; } } @@ -428,7 +434,7 @@ public class NotificationLockscreenUserManagerImpl implements * Has the given user chosen to allow their private (full) notifications to be shown even * when the lockscreen is in "public" (secure & locked) mode? */ - public boolean userAllowsPrivateNotificationsInPublic(int userHandle) { + protected boolean userAllowsPrivateNotificationsInPublic(int userHandle) { if (userHandle == UserHandle.USER_ALL) { return true; } @@ -473,10 +479,12 @@ public class NotificationLockscreenUserManagerImpl implements /** * Save the current "public" (locked and secure) state of the lockscreen. */ + @Override public void setLockscreenPublicMode(boolean publicMode, int userId) { mLockscreenPublicMode.put(userId, publicMode); } + @Override public boolean isLockscreenPublicMode(int userId) { if (userId == UserHandle.USER_ALL) { return mLockscreenPublicMode.get(mCurrentUserId, false); @@ -493,6 +501,7 @@ public class NotificationLockscreenUserManagerImpl implements * Has the given user chosen to allow notifications to be shown even when the lockscreen is in * "public" (secure & locked) mode? */ + @Override public boolean userAllowsNotificationsInPublic(int userHandle) { if (isCurrentProfile(userHandle) && userHandle != mCurrentUserId) { return true; @@ -513,36 +522,37 @@ public class NotificationLockscreenUserManagerImpl implements } /** @return true if the entry needs redaction when on the lockscreen. */ - public boolean needsRedaction(NotificationEntry ent) { + @Override + public boolean notifNeedsRedactionInPublic(NotificationEntry ent) { int userId = ent.getSbn().getUserId(); + return ent.hasSensitiveContents() && sensitiveNotifsNeedRedactionInPublic(userId); + } + @Override + public boolean sensitiveNotifsNeedRedactionInPublic(int userId) { boolean isCurrentUserRedactingNotifs = !userAllowsPrivateNotificationsInPublic(mCurrentUserId); + if (userId == mCurrentUserId) { + return isCurrentUserRedactingNotifs; + } + boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId); boolean isNotifUserRedacted = !userAllowsPrivateNotificationsInPublic(userId); // redact notifications if the current user is redacting notifications; however if the // notification is associated with a managed profile, we rely on the managed profile // setting to determine whether to redact it - boolean isNotifRedacted = (!isNotifForManagedProfile && isCurrentUserRedactingNotifs) - || isNotifUserRedacted; - - boolean notificationRequestsRedaction = - ent.getSbn().getNotification().visibility == Notification.VISIBILITY_PRIVATE; - boolean userForcesRedaction = packageHasVisibilityOverride(ent.getSbn().getKey()); + return (!isNotifForManagedProfile && isCurrentUserRedactingNotifs) || isNotifUserRedacted; + } - return userForcesRedaction || notificationRequestsRedaction && isNotifRedacted; + @Override + public void addOnNeedsRedactionInPublicChangedListener(Runnable listener) { + mOnSensitiveContentRedactionChangeListeners.addIfAbsent(listener); } - private boolean packageHasVisibilityOverride(String key) { - if (mCommonNotifCollectionLazy.get() == null) { - Log.wtf(TAG, "mEntryManager was null!", new Throwable()); - return true; - } - NotificationEntry entry = mCommonNotifCollectionLazy.get().getEntry(key); - return entry != null - && entry.getRanking().getLockscreenVisibilityOverride() - == Notification.VISIBILITY_PRIVATE; + @Override + public void removeOnNeedsRedactionInPublicChangedListener(Runnable listener) { + mOnSensitiveContentRedactionChangeListeners.remove(listener); } private void updateCurrentProfilesCache() { @@ -562,12 +572,16 @@ public class NotificationLockscreenUserManagerImpl implements for (UserChangedListener listener : mListeners) { listener.onCurrentProfilesChanged(mCurrentProfiles); } + for (Runnable listener : mOnSensitiveContentRedactionChangeListeners) { + listener.run(); + } }); } /** * If any of the profiles are in public mode. */ + @Override public boolean isAnyProfilePublicMode() { synchronized (mLock) { for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) { @@ -596,10 +610,12 @@ public class NotificationLockscreenUserManagerImpl implements /** * Returns the current user id. This can change if the user is switched. */ + @Override public int getCurrentUserId() { return mCurrentUserId; } + @Override public SparseArray<UserInfo> getCurrentProfiles() { return mCurrentProfiles; } @@ -640,7 +656,8 @@ public class NotificationLockscreenUserManagerImpl implements setLockscreenPublicMode(isProfilePublic, userId); mUsersWithSeparateWorkChallenge.put(userId, needsSeparateChallenge); } - getEntryManager().updateNotifications("NotificationLockscreenUserManager.updatePublicMode"); + mEntryManagerLazy.get() + .updateNotifications("NotificationLockscreenUserManager.updatePublicMode"); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 734bc48093b2..0d604014e8f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -326,9 +326,9 @@ public class NotificationShelf extends ActivatableNotificationView implements || child.isPinned(); boolean isLastChild = child == lastChild; final float viewStart = child.getTranslationY(); - - final float inShelfAmount = updateShelfTransformation(i, child, scrollingFast, - expandingAnimated, isLastChild); + final float shelfClipStart = getTranslationY() - mPaddingBetweenElements; + final float inShelfAmount = getAmountInShelf(i, child, scrollingFast, + expandingAnimated, isLastChild, shelfClipStart); // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) { @@ -609,10 +609,18 @@ public class NotificationShelf extends ActivatableNotificationView implements } /** - * @return the amount how much this notification is in the shelf + * @param i Index of the view in the host layout. + * @param view The current ExpandableView. + * @param scrollingFast Whether we are scrolling fast. + * @param expandingAnimated Whether we are expanding a notification. + * @param isLastChild Whether this is the last view. + * @param shelfClipStart The point at which notifications start getting clipped by the shelf. + * @return The amount how much this notification is in the shelf. + * 0f is not in shelf. 1f is completely in shelf. */ - private float updateShelfTransformation(int i, ExpandableView view, boolean scrollingFast, - boolean expandingAnimated, boolean isLastChild) { + @VisibleForTesting + public float getAmountInShelf(int i, ExpandableView view, boolean scrollingFast, + boolean expandingAnimated, boolean isLastChild, float shelfClipStart) { // Let's calculate how much the view is in the shelf float viewStart = view.getTranslationY(); @@ -635,29 +643,33 @@ public class NotificationShelf extends ActivatableNotificationView implements float viewEnd = viewStart + fullHeight; float fullTransitionAmount = 0.0f; float iconTransitionAmount = 0.0f; - float shelfStart = getTranslationY() - mPaddingBetweenElements; + + // Don't animate shelf icons during shade expansion. if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) { // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) { fullTransitionAmount = 1f; iconTransitionAmount = 1f; } - } else if (viewEnd >= shelfStart + + } else if (viewEnd >= shelfClipStart && (!mAmbientState.isUnlockHintRunning() || view.isInShelf()) && (mAmbientState.isShadeExpanded() || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) { - if (viewStart < shelfStart) { - float fullAmount = (shelfStart - viewStart) / fullHeight; + if (viewStart < shelfClipStart && Math.abs(viewStart - shelfClipStart) > 0.001f) { + // Partially clipped by shelf. + float fullAmount = (shelfClipStart - viewStart) / fullHeight; fullAmount = Math.min(1.0f, fullAmount); fullTransitionAmount = 1.0f - fullAmount; if (isLastChild) { // Reduce icon transform distance to completely fade in shelf icon // by the time the notification icon fades out, and vice versa - iconTransitionAmount = (shelfStart - viewStart) + iconTransitionAmount = (shelfClipStart - viewStart) / (iconTransformStart - viewStart); } else { - iconTransitionAmount = (shelfStart - iconTransformStart) / transformDistance; + iconTransitionAmount = (shelfClipStart - iconTransformStart) + / transformDistance; } iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f); iconTransitionAmount = 1.0f - iconTransitionAmount; @@ -772,6 +784,9 @@ public class NotificationShelf extends ActivatableNotificationView implements } private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) { + if (mShelfIcons == null) { + return null; + } return mShelfIcons.getIconState(icon); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 054543c7d2b1..30cb09d56f17 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -53,6 +53,7 @@ import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Stack; @@ -207,12 +208,9 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle || !mLockscreenUserManager.needsSeparateWorkChallenge(userId))) { userPublic = false; } - boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent); + boolean needsRedaction = mLockscreenUserManager.notifNeedsRedactionInPublic(ent); boolean sensitive = userPublic && needsRedaction; - boolean deviceSensitive = devicePublic - && !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic( - currentUserId); - ent.setSensitive(sensitive, deviceSensitive); + ent.getRow().setSensitive(sensitive); ent.getRow().setNeedsRedaction(needsRedaction); mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow()); boolean isChildInGroup = mGroupManager.isChildInGroup(ent); @@ -365,6 +363,8 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle boolean hasClearableAlertingNotifs = false; boolean hasNonClearableSilentNotifs = false; boolean hasClearableSilentNotifs = false; + HashSet<Integer> clearableAlertingSensitiveNotifUsers = new HashSet<>(); + HashSet<Integer> clearableSilentSensitiveNotifUsers = new HashSet<>(); final int childCount = mListContainer.getContainerChildCount(); int visibleTopLevelEntries = 0; for (int i = 0; i < childCount; i++) { @@ -376,10 +376,11 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle continue; } final ExpandableNotificationRow row = (ExpandableNotificationRow) child; - boolean isSilent = row.getEntry().getBucket() == BUCKET_SILENT; + NotificationEntry entry = row.getEntry(); + boolean isSilent = entry.getBucket() == BUCKET_SILENT; // NOTE: NotificationEntry.isClearable() will internally check group children to ensure // the group itself definitively clearable. - boolean isClearable = row.getEntry().isClearable(); + boolean isClearable = entry.isClearable(); visibleTopLevelEntries++; if (isSilent) { if (isClearable) { @@ -394,13 +395,24 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle hasNonClearableAlertingNotifs = true; } } + if (isClearable && entry.hasSensitiveContents()) { + int userId = entry.getSbn().getUserId(); + if (isSilent) { + clearableSilentSensitiveNotifUsers.add(userId); + } else { + clearableAlertingSensitiveNotifUsers.add(userId); + } + } } mStackController.setNotifStats(new NotifStats( visibleTopLevelEntries /* numActiveNotifs */, hasNonClearableAlertingNotifs /* hasNonClearableAlertingNotifs */, hasClearableAlertingNotifs /* hasClearableAlertingNotifs */, hasNonClearableSilentNotifs /* hasNonClearableSilentNotifs */, - hasClearableSilentNotifs /* hasClearableSilentNotifs */ + hasClearableSilentNotifs /* hasClearableSilentNotifs */, + clearableAlertingSensitiveNotifUsers /* clearableAlertingSensitiveNotifUsers */, + clearableSilentSensitiveNotifUsers /* clearableSilentSensitiveNotifUsers */ + )); Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java index a0ccd5726c75..d16e9e5d7faf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java @@ -80,7 +80,7 @@ public class DynamicPrivacyController implements KeyguardStateController.Callbac @VisibleForTesting boolean isDynamicPrivacyEnabled() { - return !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic( + return mLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic( mLockscreenUserManager.getCurrentUserId()); } @@ -95,6 +95,10 @@ public class DynamicPrivacyController implements KeyguardStateController.Callbac mListeners.add(listener); } + public void removeListener(Listener listener) { + mListeners.remove(listener); + } + /** * Is the notification shade currently in a locked down mode where it's fully showing but the * contents aren't revealed yet? diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java index 129fa5a7cc17..0c341cc92f83 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification; import android.content.Intent; -import android.service.notification.StatusBarNotification; import android.view.View; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -29,7 +28,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow */ public interface NotificationActivityStarter { /** Called when the user clicks on the surface of a notification. */ - void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row); + void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row); /** Called when the user clicks on a button in the notification guts which fires an intent. */ void startNotificationGutsIntent(Intent intent, int appUid, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index 392145ad306a..c3ce593b54fd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -104,7 +104,7 @@ public final class NotificationClicker implements View.OnClickListener { mBubblesOptional.get().collapseStack(); } - mNotificationActivityStarter.onNotificationClicked(entry.getSbn(), row); + mNotificationActivityStarter.onNotificationClicked(entry, row); } private boolean isMenuVisible(ExpandableNotificationRow row) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt index fbf033bd2291..ad3dfedcdb96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt @@ -27,7 +27,7 @@ class NotificationClickerLogger @Inject constructor( ) { fun logOnClick(entry: NotificationEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = entry.key + str1 = entry.logKey str2 = entry.ranking.channel.id }, { "CLICK $str1 (channel=$str2)" @@ -36,7 +36,7 @@ class NotificationClickerLogger @Inject constructor( fun logMenuVisible(entry: NotificationEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = entry.key + str1 = entry.logKey }, { "Ignoring click on $str1; menu is visible" }) @@ -44,7 +44,7 @@ class NotificationClickerLogger @Inject constructor( fun logParentMenuVisible(entry: NotificationEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = entry.key + str1 = entry.logKey }, { "Ignoring click on $str1; parent menu is visible" }) @@ -52,7 +52,7 @@ class NotificationClickerLogger @Inject constructor( fun logChildrenExpanded(entry: NotificationEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = entry.key + str1 = entry.logKey }, { "Ignoring click on $str1; children are expanded" }) @@ -60,7 +60,7 @@ class NotificationClickerLogger @Inject constructor( fun logGutsExposed(entry: NotificationEntry) { buffer.log(TAG, LogLevel.DEBUG, { - str1 = entry.key + str1 = entry.logKey }, { "Ignoring click on $str1; guts are exposed" }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java index c3cc97bc14a3..7cfb1571ccf0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java @@ -24,7 +24,8 @@ import android.widget.ImageView; import com.android.internal.util.ContrastColorUtil; import com.android.systemui.R; -import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.ListEntry; +import com.android.systemui.util.Compile; /** * A util class for various reusable functions @@ -74,12 +75,18 @@ public class NotificationUtils { return (int) (dimensionPixelSize * factor); } + private static final boolean INCLUDE_HASH_CODE_IN_LIST_ENTRY_LOG_KEY = false; + /** Get the notification key, reformatted for logging, for the (optional) entry */ - public static String logKey(NotificationEntry entry) { + public static String logKey(ListEntry entry) { if (entry == null) { return "null"; } - return logKey(entry.getKey()); + if (Compile.IS_DEBUG && INCLUDE_HASH_CODE_IN_LIST_ENTRY_LOG_KEY) { + return logKey(entry.getKey()) + "@" + Integer.toHexString(entry.hashCode()); + } else { + return logKey(entry.getKey()); + } } /** Removes newlines from the notification key to prettify apps that have these in the tag */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.kt new file mode 100644 index 000000000000..432bac49fa9b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.kt @@ -0,0 +1,31 @@ +/* + * 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.systemui.statusbar.notification + +import android.service.notification.StatusBarNotification +import com.android.systemui.statusbar.notification.collection.ListEntry + +/** Get the notification key, reformatted for logging, for the (optional) entry */ +val ListEntry?.logKey: String? + get() = this?.let { NotificationUtils.logKey(it) } + +/** Get the notification key, reformatted for logging, for the (optional) sbn */ +val StatusBarNotification?.logKey: String? + get() = this?.key?.let { NotificationUtils.logKey(it) } + +/** Removes newlines from the notification key to prettify apps that have these in the tag */ +fun logKey(key: String?): String? = NotificationUtils.logKey(key)
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index bcd8e594ffbd..6085096ee124 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -18,9 +18,12 @@ package com.android.systemui.statusbar.notification.collection; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; +import static android.service.notification.NotificationListenerService.REASON_ASSISTANT_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL; import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED; +import static android.service.notification.NotificationListenerService.REASON_CHANNEL_REMOVED; +import static android.service.notification.NotificationListenerService.REASON_CLEAR_DATA; import static android.service.notification.NotificationListenerService.REASON_CLICK; import static android.service.notification.NotificationListenerService.REASON_ERROR; import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION; @@ -36,9 +39,11 @@ import static android.service.notification.NotificationListenerService.REASON_TI import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED; import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED; +import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; +import static com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt.cancellationReasonDebugString; import static java.util.Objects.requireNonNull; @@ -99,6 +104,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -143,6 +149,7 @@ public class NotifCollection implements Dumpable { private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>(); private final Collection<NotificationEntry> mReadOnlyNotificationSet = Collections.unmodifiableCollection(mNotificationSet.values()); + private final HashMap<String, FutureDismissal> mFutureDismissals = new HashMap<>(); @Nullable private CollectionReadyForBuildListener mBuildListener; private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>(); @@ -511,6 +518,7 @@ public class NotifCollection implements Dumpable { cancelDismissInterception(entry); mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason)); mEventQueue.add(new CleanUpEntryEvent(entry)); + handleFutureDismissal(entry); return true; } else { return false; @@ -519,31 +527,32 @@ public class NotifCollection implements Dumpable { /** * Get the group summary entry - * @param group + * @param groupKey * @return */ @Nullable - public NotificationEntry getGroupSummary(String group) { + public NotificationEntry getGroupSummary(String groupKey) { return mNotificationSet .values() .stream() - .filter(it -> Objects.equals(it.getSbn().getGroup(), group)) + .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey)) .filter(it -> it.getSbn().getNotification().isGroupSummary()) .findFirst().orElse(null); } /** - * Checks if the entry is the only child in the logical group - * @param entry - * @return + * Checks if the entry is the only child in the logical group; + * it need not have a summary to qualify + * + * @param entry the entry to check */ public boolean isOnlyChildInGroup(NotificationEntry entry) { - String group = entry.getSbn().getGroup(); + String groupKey = entry.getSbn().getGroupKey(); return mNotificationSet.get(entry.getKey()) == entry && mNotificationSet .values() .stream() - .filter(it -> Objects.equals(it.getSbn().getGroup(), group)) + .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey)) .filter(it -> !it.getSbn().getNotification().isGroupSummary()) .count() == 1; } @@ -916,10 +925,139 @@ public class NotifCollection implements Dumpable { dispatchEventsAndRebuildList(); } + /** + * A method to alert the collection that an async operation is happening, at the end of which a + * dismissal request will be made. This method has the additional guarantee that if a parent + * notification exists for a single child, then that notification will also be dismissed. + * + * The runnable returned must be run at the end of the async operation to enact the cancellation + * + * @param entry the notification we want to dismiss + * @param cancellationReason the reason for the cancellation + * @param statsCreator the callback for generating the stats for an entry + * @return the runnable to be run when the dismissal is ready to happen + */ + public Runnable registerFutureDismissal(NotificationEntry entry, int cancellationReason, + DismissedByUserStatsCreator statsCreator) { + FutureDismissal dismissal = mFutureDismissals.get(entry.getKey()); + if (dismissal != null) { + mLogger.logFutureDismissalReused(dismissal); + return dismissal; + } + dismissal = new FutureDismissal(entry, cancellationReason, statsCreator); + mFutureDismissals.put(entry.getKey(), dismissal); + mLogger.logFutureDismissalRegistered(dismissal); + return dismissal; + } + + private void handleFutureDismissal(NotificationEntry entry) { + final FutureDismissal futureDismissal = mFutureDismissals.remove(entry.getKey()); + if (futureDismissal != null) { + futureDismissal.onSystemServerCancel(entry.mCancellationReason); + } + } + + /** A single method interface that callers can pass in when registering future dismissals */ + public interface DismissedByUserStatsCreator { + DismissedByUserStats createDismissedByUserStats(NotificationEntry entry); + } + + /** A class which tracks the double dismissal events coming in from both the system server and + * the ui */ + public class FutureDismissal implements Runnable { + private final NotificationEntry mEntry; + private final DismissedByUserStatsCreator mStatsCreator; + @Nullable + private final NotificationEntry mSummaryToDismiss; + private final String mLabel; + + private boolean mDidRun; + private boolean mDidSystemServerCancel; + + private FutureDismissal(NotificationEntry entry, @CancellationReason int cancellationReason, + DismissedByUserStatsCreator statsCreator) { + mEntry = entry; + mStatsCreator = statsCreator; + mSummaryToDismiss = fetchSummaryToDismiss(entry); + mLabel = "<FutureDismissal@" + Integer.toHexString(hashCode()) + + " entry=" + logKey(mEntry) + + " reason=" + cancellationReasonDebugString(cancellationReason) + + " summary=" + logKey(mSummaryToDismiss) + + ">"; + } + + @Nullable + private NotificationEntry fetchSummaryToDismiss(NotificationEntry entry) { + if (isOnlyChildInGroup(entry)) { + String group = entry.getSbn().getGroupKey(); + NotificationEntry summary = getGroupSummary(group); + if (summary != null && summary.isDismissable()) return summary; + } + return null; + } + + /** called when the entry has been removed from the collection */ + public void onSystemServerCancel(@CancellationReason int cancellationReason) { + Assert.isMainThread(); + if (mDidSystemServerCancel) { + mLogger.logFutureDismissalDoubleCancelledByServer(this); + return; + } + mLogger.logFutureDismissalGotSystemServerCancel(this, cancellationReason); + mDidSystemServerCancel = true; + // TODO: Internally dismiss the summary now instead of waiting for onUiCancel + } + + private void onUiCancel() { + mFutureDismissals.remove(mEntry.getKey()); + final NotificationEntry currentEntry = getEntry(mEntry.getKey()); + // generate stats for the entry before dismissing summary, which could affect state + final DismissedByUserStats stats = mStatsCreator.createDismissedByUserStats(mEntry); + // dismiss the summary (if it exists) + if (mSummaryToDismiss != null) { + final NotificationEntry currentSummary = getEntry(mSummaryToDismiss.getKey()); + if (currentSummary == mSummaryToDismiss) { + mLogger.logFutureDismissalDismissing(this, "summary"); + dismissNotification(mSummaryToDismiss, + mStatsCreator.createDismissedByUserStats(mSummaryToDismiss)); + } else { + mLogger.logFutureDismissalMismatchedEntry(this, "summary", currentSummary); + } + } + // dismiss this entry (if it is still around) + if (mDidSystemServerCancel) { + mLogger.logFutureDismissalAlreadyCancelledByServer(this); + } else if (currentEntry == mEntry) { + mLogger.logFutureDismissalDismissing(this, "entry"); + dismissNotification(mEntry, stats); + } else { + mLogger.logFutureDismissalMismatchedEntry(this, "entry", currentEntry); + } + } + + /** called when the dismissal should be completed */ + @Override + public void run() { + Assert.isMainThread(); + if (mDidRun) { + mLogger.logFutureDismissalDoubleRun(this); + return; + } + mDidRun = true; + onUiCancel(); + } + + /** provides a debug label for this instance */ + public String getLabel() { + return mLabel; + } + } + @IntDef(prefix = { "REASON_" }, value = { REASON_NOT_CANCELED, REASON_UNKNOWN, REASON_CLICK, + REASON_CANCEL, REASON_CANCEL_ALL, REASON_ERROR, REASON_PACKAGE_CHANGED, @@ -937,6 +1075,9 @@ public class NotifCollection implements Dumpable { REASON_CHANNEL_BANNED, REASON_SNOOZED, REASON_TIMEOUT, + REASON_CHANNEL_REMOVED, + REASON_CLEAR_DATA, + REASON_ASSISTANT_CANCEL, }) @Retention(RetentionPolicy.SOURCE) public @interface CancellationReason {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 4fc347a09292..e3c39ddad145 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -169,9 +169,6 @@ public final class NotificationEntry extends ListEntry { */ private boolean hasSentReply; - private boolean mSensitive = true; - private List<OnSensitivityChangedListener> mOnSensitivityChangedListeners = new ArrayList<>(); - private boolean mAutoHeadsUp; private boolean mPulseSupressed; private int mBucket = BUCKET_ALERTING; @@ -867,33 +864,29 @@ public final class NotificationEntry extends ListEntry { } /** - * Set this notification to be sensitive. - * - * @param sensitive true if the content of this notification is sensitive right now - * @param deviceSensitive true if the device in general is sensitive right now - */ - public void setSensitive(boolean sensitive, boolean deviceSensitive) { - getRow().setSensitive(sensitive, deviceSensitive); - if (sensitive != mSensitive) { - mSensitive = sensitive; - for (int i = 0; i < mOnSensitivityChangedListeners.size(); i++) { - mOnSensitivityChangedListeners.get(i).onSensitivityChanged(this); - } + * Returns the visibility of this notification on the lockscreen, taking into account both the + * notification's defined visibility, as well as the visibility override as determined by the + * device policy. + */ + public int getLockscreenVisibility() { + int setting = mRanking.getLockscreenVisibilityOverride(); + if (setting == Ranking.VISIBILITY_NO_OVERRIDE) { + setting = mSbn.getNotification().visibility; } + return setting; } - public boolean isSensitive() { - return mSensitive; - } - - /** Add a listener to be notified when the entry's sensitivity changes. */ - public void addOnSensitivityChangedListener(OnSensitivityChangedListener listener) { - mOnSensitivityChangedListeners.add(listener); - } - - /** Remove a listener that was registered above. */ - public void removeOnSensitivityChangedListener(OnSensitivityChangedListener listener) { - mOnSensitivityChangedListeners.remove(listener); + /** + * Does this notification contain sensitive content? If the user's settings specify, then this + * content would need to be redacted when the device this public. + * + * NOTE: If the notification's visibility setting is VISIBILITY_SECRET, then this will return + * false; SECRET notifications are omitted entirely when the device is public, so effectively + * the contents of the notification are not sensitive whenever the notification is actually + * visible. + */ + public boolean hasSensitiveContents() { + return getLockscreenVisibility() == Notification.VISIBILITY_PRIVATE; } public boolean isPulseSuppressed() { @@ -954,12 +947,6 @@ public final class NotificationEntry extends ListEntry { } } - /** Listener interface for {@link #addOnSensitivityChangedListener} */ - public interface OnSensitivityChangedListener { - /** Called when the sensitivity changes */ - void onSensitivityChanged(@NonNull NotificationEntry entry); - } - /** @see #getDismissState() */ public enum DismissState { /** User has not dismissed this notif or its parent */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java index 1b5e52d7f8fb..df2fe4e8511f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java @@ -93,6 +93,7 @@ public class ShadeListBuilder implements Dumpable { private final NotificationInteractionTracker mInteractionTracker; private final DumpManager mDumpManager; // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated + // TODO replace temp with collection pool for readability private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); private final boolean mAlwaysLogList; @@ -230,13 +231,7 @@ public class ShadeListBuilder implements Dumpable { mPipelineState.requireState(STATE_IDLE); mNotifSections.clear(); - NotifSectioner lastSection = null; for (NotifSectioner sectioner : sectioners) { - if (lastSection != null && lastSection.getBucket() > sectioner.getBucket()) { - throw new IllegalArgumentException("setSectioners with non contiguous sections " - + lastSection.getName() + " - " + lastSection.getBucket() + " & " - + sectioner.getName() + " - " + sectioner.getBucket()); - } final NotifSection section = new NotifSection(sectioner, mNotifSections.size()); final NotifComparator sectionComparator = section.getComparator(); mNotifSections.add(section); @@ -244,10 +239,23 @@ public class ShadeListBuilder implements Dumpable { if (sectionComparator != null) { sectionComparator.setInvalidationListener(this::onNotifComparatorInvalidated); } - lastSection = sectioner; } mNotifSections.add(new NotifSection(DEFAULT_SECTIONER, mNotifSections.size())); + + // validate sections + final ArraySet<Integer> seenBuckets = new ArraySet<>(); + int lastBucket = mNotifSections.size() > 0 + ? mNotifSections.get(0).getBucket() + : 0; + for (NotifSection section : mNotifSections) { + if (lastBucket != section.getBucket() && seenBuckets.contains(section.getBucket())) { + throw new IllegalStateException("setSectioners with non contiguous sections " + + section.getLabel() + " has an already seen bucket"); + } + lastBucket = section.getBucket(); + seenBuckets.add(lastBucket); + } } void setNotifStabilityManager(@NonNull NotifStabilityManager notifStabilityManager) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt deleted file mode 100644 index b54163d29e80..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.systemui.statusbar.notification.collection.coordinator - -import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback -import com.android.systemui.statusbar.phone.NotifActivityLaunchEvents -import dagger.Binds -import dagger.Module -import javax.inject.Inject - -/** Extends the lifetime of notifications while their activity launch animation is playing. */ -interface ActivityLaunchAnimCoordinator : Coordinator - -/** Provides an [ActivityLaunchAnimCoordinator] to [CoordinatorScope]. */ -@Module(includes = [PrivateActivityStarterCoordinatorModule::class]) -object ActivityLaunchAnimCoordinatorModule - -@Module -private interface PrivateActivityStarterCoordinatorModule { - @Binds - fun bindCoordinator(impl: ActivityLaunchAnimCoordinatorImpl): ActivityLaunchAnimCoordinator -} - -/** - * Listens for [NotifActivityLaunchEvents], and then extends the lifetimes of any notifs while their - * launch animation is playing. - */ -@CoordinatorScope -private class ActivityLaunchAnimCoordinatorImpl @Inject constructor( - private val activityLaunchEvents: NotifActivityLaunchEvents -) : ActivityLaunchAnimCoordinator { - // Tracks notification launches, and whether or not their lifetimes are extended. - private val notifsLaunchingActivities = mutableMapOf<String, Boolean>() - - private var onEndLifetimeExtensionCallback: OnEndLifetimeExtensionCallback? = null - - override fun attach(pipeline: NotifPipeline) { - activityLaunchEvents.registerListener(activityStartEventListener) - pipeline.addNotificationLifetimeExtender(extender) - } - - private val activityStartEventListener = object : NotifActivityLaunchEvents.Listener { - override fun onStartLaunchNotifActivity(entry: NotificationEntry) { - notifsLaunchingActivities[entry.key] = false - } - - override fun onFinishLaunchNotifActivity(entry: NotificationEntry) { - if (notifsLaunchingActivities.remove(entry.key) == true) { - // If we were extending the lifetime of this notification, stop. - onEndLifetimeExtensionCallback?.onEndLifetimeExtension(extender, entry) - } - } - } - - private val extender = object : NotifLifetimeExtender { - override fun getName(): String = "ActivityStarterCoordinator" - - override fun setCallback(callback: OnEndLifetimeExtensionCallback) { - onEndLifetimeExtensionCallback = callback - } - - override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean { - if (entry.key in notifsLaunchingActivities) { - // Track that we're now extending this notif - notifsLaunchingActivities[entry.key] = true - return true - } - return false - } - - override fun cancelLifetimeExtension(entry: NotificationEntry) { - if (entry.key in notifsLaunchingActivities) { - notifsLaunchingActivities[entry.key] = false - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index 3516625cc471..0b3a0dc9dd58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -56,8 +56,6 @@ class NotifCoordinatorsImpl @Inject constructor( smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, viewConfigCoordinator: ViewConfigCoordinator, visualStabilityCoordinator: VisualStabilityCoordinator, - sensitiveContentCoordinator: SensitiveContentCoordinator, - activityLaunchAnimCoordinator: ActivityLaunchAnimCoordinator ) : NotifCoordinators { private val mCoordinators: MutableList<Coordinator> = ArrayList() @@ -94,8 +92,6 @@ class NotifCoordinatorsImpl @Inject constructor( mCoordinators.add(shadeEventCoordinator) mCoordinators.add(viewConfigCoordinator) mCoordinators.add(visualStabilityCoordinator) - mCoordinators.add(sensitiveContentCoordinator) - mCoordinators.add(activityLaunchAnimCoordinator) if (notifPipelineFlags.isSmartspaceDedupingEnabled()) { mCoordinators.add(smartspaceDedupingCoordinator) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index 57fd1975e13a..56484010c213 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.collection.coordinator; -import android.annotation.NonNull; import android.annotation.Nullable; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -29,13 +28,11 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; -import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import com.android.systemui.statusbar.notification.dagger.AlertingHeader; import com.android.systemui.statusbar.notification.dagger.SilentHeader; import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; import java.util.Collections; -import java.util.List; import javax.inject.Inject; @@ -53,10 +50,10 @@ public class RankingCoordinator implements Coordinator { private final HighPriorityProvider mHighPriorityProvider; private final SectionClassifier mSectionClassifier; private final NodeController mSilentNodeController; - private final SectionHeaderController mSilentHeaderController; private final NodeController mAlertingHeaderController; - private boolean mHasSilentEntries; - private boolean mHasMinimizedEntries; + private final AlertingNotifSectioner mAlertingNotifSectioner = new AlertingNotifSectioner(); + private final SilentNotifSectioner mSilentNotifSectioner = new SilentNotifSectioner(); + private final MinimizedNotifSectioner mMinimizedNotifSectioner = new MinimizedNotifSectioner(); @Inject public RankingCoordinator( @@ -64,14 +61,12 @@ public class RankingCoordinator implements Coordinator { HighPriorityProvider highPriorityProvider, SectionClassifier sectionClassifier, @AlertingHeader NodeController alertingHeaderController, - @SilentHeader SectionHeaderController silentHeaderController, @SilentHeader NodeController silentNodeController) { mStatusBarStateController = statusBarStateController; mHighPriorityProvider = highPriorityProvider; mSectionClassifier = sectionClassifier; mAlertingHeaderController = alertingHeaderController; mSilentNodeController = silentNodeController; - mSilentHeaderController = silentHeaderController; } @Override @@ -95,8 +90,44 @@ public class RankingCoordinator implements Coordinator { return mMinimizedNotifSectioner; } - private final NotifSectioner mAlertingNotifSectioner = new NotifSectioner("Alerting", - NotificationPriorityBucketKt.BUCKET_ALERTING) { + /** + * Checks whether to filter out the given notification based the notification's Ranking object. + * NotifListBuilder invalidates the notification list each time the ranking is updated, + * so we don't need to explicitly invalidate this filter on ranking update. + */ + private final NotifFilter mSuspendedFilter = new NotifFilter("IsSuspendedFilter") { + @Override + public boolean shouldFilterOut(NotificationEntry entry, long now) { + return entry.getRanking().isSuspended(); + } + }; + + private final NotifFilter mDndVisualEffectsFilter = new NotifFilter( + "DndSuppressingVisualEffects") { + @Override + public boolean shouldFilterOut(NotificationEntry entry, long now) { + if (mStatusBarStateController.isDozing() && entry.shouldSuppressAmbient()) { + return true; + } + + return !mStatusBarStateController.isDozing() && entry.shouldSuppressNotificationList(); + } + }; + + private final StatusBarStateController.StateListener mStatusBarStateCallback = + new StatusBarStateController.StateListener() { + @Override + public void onDozingChanged(boolean isDozing) { + mDndVisualEffectsFilter.invalidateList(); + } + }; + + private class AlertingNotifSectioner extends NotifSectioner { + + AlertingNotifSectioner() { + super("Alerting", NotificationPriorityBucketKt.BUCKET_ALERTING); + } + @Override public boolean isInSection(ListEntry entry) { return mHighPriorityProvider.isHighPriority(entry); @@ -111,10 +142,14 @@ public class RankingCoordinator implements Coordinator { } return null; } - }; + } + + private class SilentNotifSectioner extends NotifSectioner { + + SilentNotifSectioner() { + super("Silent", NotificationPriorityBucketKt.BUCKET_SILENT); + } - private final NotifSectioner mSilentNotifSectioner = new NotifSectioner("Silent", - NotificationPriorityBucketKt.BUCKET_SILENT) { @Override public boolean isInSection(ListEntry entry) { return !mHighPriorityProvider.isHighPriority(entry) @@ -126,24 +161,14 @@ public class RankingCoordinator implements Coordinator { public NodeController getHeaderNodeController() { return mSilentNodeController; } + } - @Nullable - @Override - public void onEntriesUpdated(@NonNull List<ListEntry> entries) { - mHasSilentEntries = false; - for (int i = 0; i < entries.size(); i++) { - if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) { - mHasSilentEntries = true; - break; - } - } - mSilentHeaderController.setClearSectionEnabled( - mHasSilentEntries | mHasMinimizedEntries); + private class MinimizedNotifSectioner extends NotifSectioner { + + MinimizedNotifSectioner() { + super("Minimized", NotificationPriorityBucketKt.BUCKET_SILENT); } - }; - private final NotifSectioner mMinimizedNotifSectioner = new NotifSectioner("Minimized", - NotificationPriorityBucketKt.BUCKET_SILENT) { @Override public boolean isInSection(ListEntry entry) { return !mHighPriorityProvider.isHighPriority(entry) @@ -155,51 +180,5 @@ public class RankingCoordinator implements Coordinator { public NodeController getHeaderNodeController() { return mSilentNodeController; } - - @Nullable - @Override - public void onEntriesUpdated(@NonNull List<ListEntry> entries) { - mHasMinimizedEntries = false; - for (int i = 0; i < entries.size(); i++) { - if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) { - mHasMinimizedEntries = true; - break; - } - } - mSilentHeaderController.setClearSectionEnabled( - mHasSilentEntries | mHasMinimizedEntries); - } - }; - - /** - * Checks whether to filter out the given notification based the notification's Ranking object. - * NotifListBuilder invalidates the notification list each time the ranking is updated, - * so we don't need to explicitly invalidate this filter on ranking update. - */ - private final NotifFilter mSuspendedFilter = new NotifFilter("IsSuspendedFilter") { - @Override - public boolean shouldFilterOut(NotificationEntry entry, long now) { - return entry.getRanking().isSuspended(); - } - }; - - private final NotifFilter mDndVisualEffectsFilter = new NotifFilter( - "DndSuppressingVisualEffects") { - @Override - public boolean shouldFilterOut(NotificationEntry entry, long now) { - if (mStatusBarStateController.isDozing() && entry.shouldSuppressAmbient()) { - return true; - } - - return !mStatusBarStateController.isDozing() && entry.shouldSuppressNotificationList(); - } - }; - - private final StatusBarStateController.StateListener mStatusBarStateCallback = - new StatusBarStateController.StateListener() { - @Override - public void onDozingChanged(boolean isDozing) { - mDndVisualEffectsFilter.invalidateList(); - } - }; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt deleted file mode 100644 index 3f8a39f62dfb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2021 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.systemui.statusbar.notification.collection.coordinator - -import android.os.UserHandle -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.statusbar.NotificationLockscreenUserManager -import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.notification.DynamicPrivacyController -import com.android.systemui.statusbar.notification.collection.GroupEntry -import com.android.systemui.statusbar.notification.collection.ListEntry -import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope -import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator -import com.android.systemui.statusbar.policy.KeyguardStateController -import dagger.Binds -import dagger.Module -import javax.inject.Inject - -@Module(includes = [PrivateSensitiveContentCoordinatorModule::class]) -interface SensitiveContentCoordinatorModule - -@Module -private interface PrivateSensitiveContentCoordinatorModule { - @Binds - fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator -} - -/** Coordinates re-inflation and post-processing of sensitive notification content. */ -interface SensitiveContentCoordinator : Coordinator - -@CoordinatorScope -private class SensitiveContentCoordinatorImpl @Inject constructor( - private val dynamicPrivacyController: DynamicPrivacyController, - private val lockscreenUserManager: NotificationLockscreenUserManager, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val statusBarStateController: StatusBarStateController, - private val keyguardStateController: KeyguardStateController -) : Invalidator("SensitiveContentInvalidator"), - SensitiveContentCoordinator, - DynamicPrivacyController.Listener, - OnBeforeRenderListListener { - - override fun attach(pipeline: NotifPipeline) { - dynamicPrivacyController.addListener(this) - pipeline.addOnBeforeRenderListListener(this) - pipeline.addPreRenderInvalidator(this) - } - - override fun onDynamicPrivacyChanged(): Unit = invalidateList() - - override fun onBeforeRenderList(entries: List<ListEntry>) { - if (keyguardStateController.isKeyguardGoingAway() || - statusBarStateController.getState() == StatusBarState.KEYGUARD && - keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing( - KeyguardUpdateMonitor.getCurrentUser())) { - // don't update yet if: - // - the keyguard is currently going away - // - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash) - - // TODO(b/206118999): merge this class with KeyguardCoordinator which ensures the - // dependent state changes invalidate the pipeline - return - } - - val currentUserId = lockscreenUserManager.currentUserId - val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId) - val deviceSensitive = devicePublic && - !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId) - val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked - for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) { - val notifUserId = entry.sbn.user.identifier - val userLockscreen = devicePublic || - lockscreenUserManager.isLockscreenPublicMode(notifUserId) - val userPublic = when { - // if we're not on the lockscreen, we're definitely private - !userLockscreen -> false - // we are on the lockscreen, so unless we're dynamically unlocked, we're - // definitely public - !dynamicallyUnlocked -> true - // we're dynamically unlocked, but check if the notification needs - // a separate challenge if it's from a work profile - else -> when (notifUserId) { - currentUserId -> false - UserHandle.USER_ALL -> false - else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId) - } - } - val needsRedaction = lockscreenUserManager.needsRedaction(entry) - val isSensitive = userPublic && needsRedaction - entry.setSensitive(isSensitive, deviceSensitive) - } - } -} - -private fun extractAllRepresentativeEntries( - entries: List<ListEntry> -): Sequence<NotificationEntry> = - entries.asSequence().flatMap(::extractAllRepresentativeEntries) - -private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<NotificationEntry> = - sequence { - listEntry.representativeEntry?.let { yield(it) } - if (listEntry is GroupEntry) { - yieldAll(extractAllRepresentativeEntries(listEntry.children)) - } - } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 1c96e8ceb27f..2374e9c58177 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -50,6 +50,8 @@ class StackCoordinator @Inject internal constructor( var hasClearableAlertingNotifs = false var hasNonClearableSilentNotifs = false var hasClearableSilentNotifs = false + val clearableAlertingSensitiveNotifUsers = mutableSetOf<Int>() + val clearableSilentSensitiveNotifUsers = mutableSetOf<Int>() entries.forEach { val section = checkNotNull(it.section) { "Null section for ${it.key}" } val entry = checkNotNull(it.representativeEntry) { "Null notif entry for ${it.key}" } @@ -63,13 +65,22 @@ class StackCoordinator @Inject internal constructor( !isSilent && isClearable -> hasClearableAlertingNotifs = true !isSilent && !isClearable -> hasNonClearableAlertingNotifs = true } + if (isClearable && entry.hasSensitiveContents()) { + if (isSilent) { + clearableSilentSensitiveNotifUsers.add(entry.sbn.userId) + } else { + clearableAlertingSensitiveNotifUsers.add(entry.sbn.userId) + } + } } return NotifStats( numActiveNotifs = entries.size, hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs, hasClearableAlertingNotifs = hasClearableAlertingNotifs, hasNonClearableSilentNotifs = hasNonClearableSilentNotifs, - hasClearableSilentNotifs = hasClearableSilentNotifs + hasClearableSilentNotifs = hasClearableSilentNotifs, + clearableAlertingSensitiveNotifUsers = clearableAlertingSensitiveNotifUsers, + clearableSilentSensitiveNotifUsers = clearableSilentSensitiveNotifUsers ) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt index 274affd9da43..839cf0d7ef92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt @@ -17,10 +17,8 @@ package com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.collection.coordinator.ActivityLaunchAnimCoordinatorModule import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinatorsImpl -import com.android.systemui.statusbar.notification.collection.coordinator.SensitiveContentCoordinatorModule import dagger.Binds import dagger.Module import dagger.Provides @@ -49,8 +47,6 @@ interface CoordinatorsSubcomponent { } @Module(includes = [ - ActivityLaunchAnimCoordinatorModule::class, - SensitiveContentCoordinatorModule::class, ]) private abstract class InternalCoordinatorsModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 4c7b2bbfb6d9..6e96aad776d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.collection.inflation; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; + import static java.util.Objects.requireNonNull; import android.annotation.Nullable; @@ -249,10 +251,13 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { RowContentBindParams params = mRowContentBindStage.getStageParams(entry); params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); params.setUseLowPriority(isLowPriority); - - // TODO: Replace this API with RowContentBindParams directly. Also move to a separate - // redaction controller. - row.setNeedsRedaction(mNotificationLockscreenUserManager.needsRedaction(entry)); + boolean needsRedaction = + mNotificationLockscreenUserManager.notifNeedsRedactionInPublic(entry); + if (needsRedaction) { + params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC); + } else { + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); + } params.rebindAllContentViews(); mRowContentBindStage.requestRebind(entry, en -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java index 3bd91b5c8480..7dd3672a6e30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java @@ -18,17 +18,17 @@ package com.android.systemui.statusbar.notification.collection.inflation; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; -import android.annotation.Nullable; import android.os.SystemClock; -import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; +import androidx.annotation.NonNull; + import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator; import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; -import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback; import com.android.systemui.statusbar.policy.HeadsUpManager; @@ -43,54 +43,33 @@ public class OnUserInteractionCallbackImpl implements OnUserInteractionCallback private final HeadsUpManager mHeadsUpManager; private final StatusBarStateController mStatusBarStateController; private final VisualStabilityCoordinator mVisualStabilityCoordinator; - private final GroupMembershipManager mGroupMembershipManager; public OnUserInteractionCallbackImpl( NotificationVisibilityProvider visibilityProvider, NotifCollection notifCollection, HeadsUpManager headsUpManager, StatusBarStateController statusBarStateController, - VisualStabilityCoordinator visualStabilityCoordinator, - GroupMembershipManager groupMembershipManager + VisualStabilityCoordinator visualStabilityCoordinator ) { mVisibilityProvider = visibilityProvider; mNotifCollection = notifCollection; mHeadsUpManager = headsUpManager; mStatusBarStateController = statusBarStateController; mVisualStabilityCoordinator = visualStabilityCoordinator; - mGroupMembershipManager = groupMembershipManager; } - /** - * Callback triggered when a user: - * 1. Manually dismisses a notification {@see ExpandableNotificationRow}. - * 2. Clicks on a notification with flag {@link android.app.Notification#FLAG_AUTO_CANCEL}. - * {@see StatusBarNotificationActivityStarter} - */ - @Override - public void onDismiss( - NotificationEntry entry, - @NotificationListenerService.NotificationCancelReason int cancellationReason, - @Nullable NotificationEntry groupSummaryToDismiss - ) { + @NonNull + private DismissedByUserStats getDismissedByUserStats(NotificationEntry entry) { int dismissalSurface = NotificationStats.DISMISSAL_SHADE; if (mHeadsUpManager.isAlerting(entry.getKey())) { dismissalSurface = NotificationStats.DISMISSAL_PEEK; } else if (mStatusBarStateController.isDozing()) { dismissalSurface = NotificationStats.DISMISSAL_AOD; } - - if (groupSummaryToDismiss != null) { - onDismiss(groupSummaryToDismiss, cancellationReason, null); - } - - mNotifCollection.dismissNotification( - entry, - new DismissedByUserStats( - dismissalSurface, - DISMISS_SENTIMENT_NEUTRAL, - mVisibilityProvider.obtain(entry, true)) - ); + return new DismissedByUserStats( + dismissalSurface, + DISMISS_SENTIMENT_NEUTRAL, + mVisibilityProvider.obtain(entry, true)); } @Override @@ -100,19 +79,11 @@ public class OnUserInteractionCallbackImpl implements OnUserInteractionCallback SystemClock.uptimeMillis()); } - /** - * @param entry that is being dismissed - * @return the group summary to dismiss along with this entry if this is the last entry in - * the group. Else, returns null. - */ + @NonNull @Override - @Nullable - public NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry) { - String group = entry.getSbn().getGroup(); - if (mNotifCollection.isOnlyChildInGroup(entry)) { - NotificationEntry summary = mNotifCollection.getGroupSummary(group); - if (summary != null && summary.isDismissable()) return summary; - } - return null; + public Runnable registerFutureDismissal(@NonNull NotificationEntry entry, + @CancellationReason int cancellationReason) { + return mNotifCollection.registerFutureDismissal( + entry, cancellationReason, this::getDismissedByUserStats); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java index 8daf8be0cc8f..103b14b09e9c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java @@ -18,12 +18,15 @@ package com.android.systemui.statusbar.notification.collection.legacy; import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; -import android.annotation.Nullable; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -68,8 +71,7 @@ public class OnUserInteractionCallbackImplLegacy implements OnUserInteractionCal * along with this dismissal. If null, does not additionally * dismiss any notifications. */ - @Override - public void onDismiss( + private void onDismiss( NotificationEntry entry, @NotificationListenerService.NotificationCancelReason int cancellationReason, @Nullable NotificationEntry groupSummaryToDismiss @@ -106,14 +108,21 @@ public class OnUserInteractionCallbackImplLegacy implements OnUserInteractionCal * @return the group summary to dismiss along with this entry if this is the last entry in * the group. Else, returns null. */ - @Override @Nullable - public NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry) { + private NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry) { if (mGroupMembershipManager.isOnlyChildInGroup(entry)) { NotificationEntry groupSummary = mGroupMembershipManager.getLogicalGroupSummary(entry); return groupSummary.isDismissable() ? groupSummary : null; } return null; } + + @Override + @NonNull + public Runnable registerFutureDismissal(@NonNull NotificationEntry entry, + @CancellationReason int cancellationReason) { + NotificationEntry groupSummaryToDismiss = getGroupSummaryToDismiss(entry); + return () -> onDismiss(entry, cancellationReason, groupSummaryToDismiss); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt index 7302de57e955..7e7936717b84 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt @@ -28,7 +28,9 @@ import com.android.systemui.log.LogLevel.WTF import com.android.systemui.log.dagger.NotificationLog import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason +import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject fun cancellationReasonDebugString(@CancellationReason reason: Int) = @@ -36,6 +38,7 @@ fun cancellationReasonDebugString(@CancellationReason reason: Int) = -1 -> "REASON_NOT_CANCELED" // NotifCollection.REASON_NOT_CANCELED NotifCollection.REASON_UNKNOWN -> "REASON_UNKNOWN" NotificationListenerService.REASON_CLICK -> "REASON_CLICK" + NotificationListenerService.REASON_CANCEL -> "REASON_CANCEL" NotificationListenerService.REASON_CANCEL_ALL -> "REASON_CANCEL_ALL" NotificationListenerService.REASON_ERROR -> "REASON_ERROR" NotificationListenerService.REASON_PACKAGE_CHANGED -> "REASON_PACKAGE_CHANGED" @@ -53,6 +56,9 @@ fun cancellationReasonDebugString(@CancellationReason reason: Int) = NotificationListenerService.REASON_CHANNEL_BANNED -> "REASON_CHANNEL_BANNED" NotificationListenerService.REASON_SNOOZED -> "REASON_SNOOZED" NotificationListenerService.REASON_TIMEOUT -> "REASON_TIMEOUT" + NotificationListenerService.REASON_CHANNEL_REMOVED -> "REASON_CHANNEL_REMOVED" + NotificationListenerService.REASON_CLEAR_DATA -> "REASON_CLEAR_DATA" + NotificationListenerService.REASON_ASSISTANT_CANCEL -> "REASON_ASSISTANT_CANCEL" else -> "unknown" } @@ -241,6 +247,81 @@ class NotifCollectionLogger @Inject constructor( "ERROR suppressed due to initialization forgiveness: $str1" }) } + + fun logFutureDismissalReused(dismissal: FutureDismissal) { + buffer.log(TAG, INFO, { + str1 = dismissal.label + }, { + "Reusing existing registration: $str1" + }) + } + + fun logFutureDismissalRegistered(dismissal: FutureDismissal) { + buffer.log(TAG, DEBUG, { + str1 = dismissal.label + }, { + "Registered: $str1" + }) + } + + fun logFutureDismissalDoubleCancelledByServer(dismissal: FutureDismissal) { + buffer.log(TAG, WARNING, { + str1 = dismissal.label + }, { + "System server double cancelled: $str1" + }) + } + + fun logFutureDismissalDoubleRun(dismissal: FutureDismissal) { + buffer.log(TAG, WARNING, { + str1 = dismissal.label + }, { + "Double run: $str1" + }) + } + + fun logFutureDismissalAlreadyCancelledByServer(dismissal: FutureDismissal) { + buffer.log(TAG, DEBUG, { + str1 = dismissal.label + }, { + "Ignoring: entry already cancelled by server: $str1" + }) + } + + fun logFutureDismissalGotSystemServerCancel( + dismissal: FutureDismissal, + @CancellationReason cancellationReason: Int + ) { + buffer.log(TAG, DEBUG, { + str1 = dismissal.label + int1 = cancellationReason + }, { + "SystemServer cancelled: $str1 reason=${cancellationReasonDebugString(int1)}" + }) + } + + fun logFutureDismissalDismissing(dismissal: FutureDismissal, type: String) { + buffer.log(TAG, DEBUG, { + str1 = dismissal.label + str2 = type + }, { + "Dismissing $str2 for: $str1" + }) + } + + fun logFutureDismissalMismatchedEntry( + dismissal: FutureDismissal, + type: String, + latestEntry: NotificationEntry? + ) { + buffer.log(TAG, WARNING, { + str1 = dismissal.label + str2 = type + str3 = latestEntry.logKey + }, { + "Mismatch: current $str2 is $str3 for: $str1" + }) + } } private const val TAG = "NotifCollection" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt index 6db544c77f87..8be710c8842c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt @@ -57,7 +57,6 @@ class NodeSpecBuilder( var currentSection: NotifSection? = null val prevSections = mutableSetOf<NotifSection?>() - var lastSection: NotifSection? = null val showHeaders = sectionHeaderVisibilityProvider.sectionHeadersVisible val sectionOrder = mutableListOf<NotifSection?>() val sectionHeaders = mutableMapOf<NotifSection?, NodeController?>() @@ -65,15 +64,6 @@ class NodeSpecBuilder( for (entry in notifList) { val section = entry.section!! - - lastSection?.let { - if (it.bucket > section.bucket) { - throw IllegalStateException("buildNodeSpec with non contiguous section " + - "buckets ${it.sectioner.name} - ${it.bucket} & " + - "${it.sectioner.name} - ${it.bucket}") - } - } - lastSection = section if (prevSections.contains(section)) { throw java.lang.RuntimeException("Section ${section.label} has been duplicated") } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt index b6278d1d5f01..580d853dc5e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt @@ -28,11 +28,21 @@ data class NotifStats( val hasNonClearableAlertingNotifs: Boolean, val hasClearableAlertingNotifs: Boolean, val hasNonClearableSilentNotifs: Boolean, - val hasClearableSilentNotifs: Boolean + val hasClearableSilentNotifs: Boolean, + val clearableAlertingSensitiveNotifUsers: Set<Int>, + val clearableSilentSensitiveNotifUsers: Set<Int> ) { companion object { @JvmStatic - val empty = NotifStats(0, false, false, false, false) + val empty = NotifStats( + numActiveNotifs = 0, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = false, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = false, + clearableAlertingSensitiveNotifUsers = emptySet(), + clearableSilentSensitiveNotifUsers = emptySet(), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt index 2a9cfd034dce..f58918fe6f80 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt @@ -182,4 +182,4 @@ annotation class HeaderClickAction @Scope @Retention(AnnotationRetention.BINARY) -annotation class SectionHeaderScope
\ No newline at end of file +annotation class SectionHeaderScope diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index d96590a82547..c9c7fe9e0ab6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -88,7 +88,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationSectionsMan import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBypassController; -import com.android.systemui.statusbar.phone.NotifActivityLaunchEventsModule; import com.android.systemui.statusbar.phone.NotifPanelEventsModule; import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.policy.HeadsUpManager; @@ -111,7 +110,6 @@ import dagger.Provides; @Module(includes = { CoordinatorsModule.class, KeyguardNotificationVisibilityProviderModule.class, - NotifActivityLaunchEventsModule.class, NotifPanelEventsModule.class, NotifPipelineChoreographerModule.class, NotificationSectionHeadersModule.class, @@ -350,8 +348,7 @@ public interface NotificationsModule { notifCollection.get(), headsUpManager, statusBarStateController, - visualStabilityCoordinator.get(), - groupMembershipManagerLazy.get()) + visualStabilityCoordinator.get()) : new OnUserInteractionCallbackImplLegacy( entryManager, visibilityProvider.get(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index d8965418b4c4..015e3d8cd553 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -28,6 +28,7 @@ import android.widget.ImageView import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -49,31 +50,29 @@ import javax.inject.Inject class IconManager @Inject constructor( private val notifCollection: CommonNotifCollection, private val launcherApps: LauncherApps, - private val iconBuilder: IconBuilder + private val iconBuilder: IconBuilder, + private val notifLockscreenUserManager: NotificationLockscreenUserManager ) : ConversationIconManager { private var unimportantConversationKeys: Set<String> = emptySet() fun attach() { notifCollection.addCollectionListener(entryListener) + notifLockscreenUserManager.addOnNeedsRedactionInPublicChangedListener(sensitivityListener) } private val entryListener = object : NotifCollectionListener { - override fun onEntryInit(entry: NotificationEntry) { - entry.addOnSensitivityChangedListener(sensitivityListener) - } - - override fun onEntryCleanUp(entry: NotificationEntry) { - entry.removeOnSensitivityChangedListener(sensitivityListener) - } - override fun onRankingApplied() { // rankings affect whether a conversation is important, which can change the icons recalculateForImportantConversationChange() } } - private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener { - entry -> updateIconsSafe(entry) + private val sensitivityListener = Runnable { + for (entry in notifCollection.allNotifs) { + if (entry.hasSensitiveContents()) { + updateIconsSafe(entry) + } + } } private fun recalculateForImportantConversationChange() { @@ -182,12 +181,16 @@ class IconManager @Inject constructor( } } + private inline val NotificationEntry.needsRedactionInPublic: Boolean get() = + hasSensitiveContents() && + notifLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(sbn.userId) + @Throws(InflationException::class) private fun getIconDescriptors( entry: NotificationEntry ): Pair<StatusBarIcon, StatusBarIcon> { val iconDescriptor = getIconDescriptor(entry, false /* redact */) - val sensitiveDescriptor = if (entry.isSensitive) { + val sensitiveDescriptor = if (entry.needsRedactionInPublic) { getIconDescriptor(entry, true /* redact */) } else { iconDescriptor @@ -310,7 +313,7 @@ class IconManager @Inject constructor( iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon) return isImportantConversation(entry) && !isSmallIcon && - (!usedInSensitiveContext || !entry.isSensitive) + (!usedInSensitiveContext || !entry.needsRedactionInPublic) } private fun isImportantConversation(entry: NotificationEntry): Boolean { @@ -338,4 +341,4 @@ interface ConversationIconManager { * of a group from which the priority notification has been removed. */ fun setUnimportantConversations(keys: Collection<String>) -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index dff8c47e36e0..d5088acaa61c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.row; import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY; -import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; @@ -34,8 +33,6 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.role.RoleManager; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Canvas; @@ -51,7 +48,6 @@ import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Trace; -import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.AttributeSet; @@ -60,6 +56,7 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.MathUtils; import android.util.Property; +import android.util.Slog; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -93,8 +90,8 @@ import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.notification.AboveShelfChangedListener; -import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.FeedbackIcon; +import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; @@ -112,7 +109,6 @@ import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.notification.stack.SwipeableView; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; @@ -206,7 +202,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** Are we showing the "public" version */ private boolean mShowingPublic; private boolean mSensitive; - private boolean mSensitiveHiddenInGeneral; private boolean mShowingPublicInitialized; private boolean mHideSensitiveForIntrinsicHeight; private float mHeaderVisibleAmount = DEFAULT_HEADER_VISIBLE_AMOUNT; @@ -364,9 +359,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Nullable private OnExpansionChangedListener mExpansionChangedListener; @Nullable private Runnable mOnIntrinsicHeightReachedRunnable; - private SystemNotificationAsyncTask mSystemNotificationAsyncTask = - new SystemNotificationAsyncTask(); - private float mTopRoundnessDuringLaunchAnimation; private float mBottomRoundnessDuringLaunchAnimation; @@ -518,45 +510,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** - * Caches whether or not this row contains a system notification. Note, this is only cached - * once per notification as the packageInfo can't technically change for a notification row. - */ - private void cacheIsSystemNotification() { - //TODO: This probably shouldn't be in ExpandableNotificationRow - if (mEntry != null && mEntry.mIsSystemNotification == null) { - if (mSystemNotificationAsyncTask.getStatus() == AsyncTask.Status.PENDING) { - // Run async task once, only if it hasn't already been executed. Note this is - // executed in serial - no need to parallelize this small task. - mSystemNotificationAsyncTask.execute(); - } - } - } - - /** * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif * or is in an allowList). */ public boolean getIsNonblockable() { - // If the SystemNotifAsyncTask hasn't finished running or retrieved a value, we'll try once - // again, but in-place on the main thread this time. This should rarely ever get called. - if (mEntry != null && mEntry.mIsSystemNotification == null) { - if (DEBUG) { - Log.d(TAG, "Retrieving isSystemNotification on main thread"); - } - mSystemNotificationAsyncTask.cancel(true /* mayInterruptIfRunning */); - mEntry.mIsSystemNotification = isSystemNotification(mContext, mEntry.getSbn()); + if (mEntry == null || mEntry.getChannel() == null) { + Log.w(TAG, "missing entry or channel"); + return true; } - - boolean isNonblockable = mEntry.getChannel().isImportanceLockedByCriticalDeviceFunction(); - - if (!isNonblockable && mEntry != null && mEntry.mIsSystemNotification != null) { - if (mEntry.mIsSystemNotification) { - if (mEntry.getChannel() != null && !mEntry.getChannel().isBlockable()) { - isNonblockable = true; - } - } + if (mEntry.getChannel().isImportanceLockedByCriticalDeviceFunction() + && !mEntry.getChannel().isBlockable()) { + return true; } - return isNonblockable; + + return false; } private boolean isConversation() { @@ -1456,8 +1423,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView dismiss(fromAccessibility); if (mEntry.isDismissable()) { if (mOnUserInteractionCallback != null) { - mOnUserInteractionCallback.onDismiss(mEntry, REASON_CANCEL, - mOnUserInteractionCallback.getGroupSummaryToDismiss(mEntry)); + mOnUserInteractionCallback.registerFutureDismissal(mEntry, REASON_CANCEL).run(); } } } @@ -1538,6 +1504,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mUseIncreasedHeadsUpHeight = use; } + // TODO: remove this method and mNeedsRedaction entirely once the old pipeline is gone public void setNeedsRedaction(boolean needsRedaction) { // TODO: Move inflation logic out of this call and remove this method if (mNeedsRedaction != needsRedaction) { @@ -1626,8 +1593,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mBubblesManagerOptional = bubblesManagerOptional; mNotificationGutsManager = gutsManager; mMetricsLogger = metricsLogger; - - cacheIsSystemNotification(); } private void initDimens() { @@ -2622,9 +2587,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked()); } - public void setSensitive(boolean sensitive, boolean hideSensitive) { + public void setSensitive(boolean sensitive) { mSensitive = sensitive; - mSensitiveHiddenInGeneral = hideSensitive; } @Override @@ -2714,7 +2678,15 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * see {@link NotificationEntry#isDismissable()}. */ public boolean canViewBeDismissed() { - return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + // Entry not dismissable. + if (!mEntry.isDismissable()) { + return false; + } + // Entry shouldn't be showing the public layout, it can be dismissed. + if (!shouldShowPublic()) { + return true; + } + return false; } /** @@ -2723,7 +2695,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * clearability see {@link NotificationEntry#isClearable()}. */ public boolean canViewBeCleared() { - return mEntry.isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral); + return mEntry.isClearable() && !shouldShowPublic(); } private boolean shouldShowPublic() { @@ -3487,10 +3459,28 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.print(", translation: " + getTranslation()); pw.print(", removed: " + isRemoved()); pw.print(", expandAnimationRunning: " + mExpandAnimationRunning); - NotificationContentView showingLayout = getShowingLayout(); - pw.print(", privateShowing: " + (showingLayout == mPrivateLayout)); - pw.println(); - showingLayout.dump(pw, args); + pw.print(", sensitive: " + mSensitive); + pw.print(", hideSensitiveForIntrinsicHeight: " + mHideSensitiveForIntrinsicHeight); + pw.println(", privateShowing: " + !shouldShowPublic()); + pw.print("privateLayout: "); + if (mPrivateLayout != null) { + pw.println(); + DumpUtilsKt.withIncreasedIndent(pw, () -> { + mPrivateLayout.dump(pw, args); + mPrivateLayout.dumpSmartReplies(pw); + }); + } else { + pw.println("null"); + } + pw.print("publicLayout: "); + if (mPublicLayout != null) { + pw.println(); + DumpUtilsKt.withIncreasedIndent(pw, () -> { + mPublicLayout.dump(pw, args); + }); + } else { + pw.println("null"); + } if (getViewState() != null) { getViewState().dump(pw, args); @@ -3516,31 +3506,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } pw.decreaseIndent(); pw.println("}"); - } else if (mPrivateLayout != null) { - mPrivateLayout.dumpSmartReplies(pw); } }); } - /** - * Background task for executing IPCs to check if the notification is a system notification. The - * output is used for both the blocking helper and the notification info. - */ - private class SystemNotificationAsyncTask extends AsyncTask<Void, Void, Boolean> { - - @Override - protected Boolean doInBackground(Void... voids) { - return isSystemNotification(mContext, mEntry.getSbn()); - } - - @Override - protected void onPostExecute(Boolean result) { - if (mEntry != null) { - mEntry.mIsSystemNotification = result; - } - } - } - private void setTargetPoint(Point p) { mTargetPoint = p; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 599039d46556..a60026c7a97b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -35,6 +35,7 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.notification.FeedbackIcon; @@ -69,6 +70,7 @@ import javax.inject.Named; public class ExpandableNotificationRowController implements NotifViewController { private static final String TAG = "NotifRowController"; private final ExpandableNotificationRow mView; + private final NotificationLockscreenUserManager mLockscreenUserManager; private final NotificationListContainer mListContainer; private final RemoteInputViewSubcomponent.Factory mRemoteInputViewSubcomponentFactory; private final ActivatableNotificationViewController mActivatableNotificationViewController; @@ -86,7 +88,6 @@ public class ExpandableNotificationRowController implements NotifViewController private final ExpandableNotificationRow.OnExpandClickListener mOnExpandClickListener; private final StatusBarStateController mStatusBarStateController; private final MetricsLogger mMetricsLogger; - private final ExpandableNotificationRow.ExpansionLogger mExpansionLogger = this::logNotificationExpansion; private final ExpandableNotificationRow.CoordinateOnClickListener mOnFeedbackClickListener; @@ -100,12 +101,12 @@ public class ExpandableNotificationRowController implements NotifViewController private final Optional<BubblesManager> mBubblesManagerOptional; private final SmartReplyConstants mSmartReplyConstants; private final SmartReplyController mSmartReplyController; - private final ExpandableNotificationRowDragController mDragController; @Inject public ExpandableNotificationRowController( ExpandableNotificationRow view, + NotificationLockscreenUserManager lockscreenUserManager, ActivatableNotificationViewController activatableNotificationViewController, RemoteInputViewSubcomponent.Factory rivSubcomponentFactory, MetricsLogger metricsLogger, @@ -135,6 +136,7 @@ public class ExpandableNotificationRowController implements NotifViewController Optional<BubblesManager> bubblesManagerOptional, ExpandableNotificationRowDragController dragController) { mView = view; + mLockscreenUserManager = lockscreenUserManager; mListContainer = listContainer; mRemoteInputViewSubcomponentFactory = rivSubcomponentFactory; mActivatableNotificationViewController = activatableNotificationViewController; @@ -214,6 +216,10 @@ public class ExpandableNotificationRowController implements NotifViewController mView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); } + mLockscreenUserManager + .addOnNeedsRedactionInPublicChangedListener(mNeedsRedactionListener); + mNeedsRedactionListener.run(); + mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { @@ -232,6 +238,14 @@ public class ExpandableNotificationRowController implements NotifViewController }); } + private final Runnable mNeedsRedactionListener = new Runnable() { + @Override + public void run() { + mView.setSensitive( + mLockscreenUserManager.notifNeedsRedactionInPublic(mView.getEntry())); + } + }; + private final StatusBarStateController.StateListener mStatusBarStateListener = new StatusBarStateController.StateListener() { @Override @@ -333,4 +347,5 @@ public class ExpandableNotificationRowController implements NotifViewController public void setFeedbackIcon(@Nullable FeedbackIcon icon) { mView.setFeedbackIcon(icon); } + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index ba26cfaa30b4..a7c6bfb0289c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -61,6 +61,7 @@ import com.android.systemui.statusbar.policy.SmartReplyStateInflaterKt; import com.android.systemui.statusbar.policy.SmartReplyView; import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent; import com.android.systemui.util.Compile; +import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.wmshell.BubblesManager; import java.io.PrintWriter; @@ -1994,22 +1995,33 @@ public class NotificationContentView extends FrameLayout implements Notification } } - public void dump(PrintWriter pw, String[] args) { + public void dump(PrintWriter pwOriginal, String[] args) { + IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); pw.print("contentView visibility: " + getVisibility()); pw.print(", alpha: " + getAlpha()); pw.print(", clipBounds: " + getClipBounds()); pw.print(", contentHeight: " + mContentHeight); - pw.print(", visibleType: " + mVisibleType); - View view = getViewForVisibleType(mVisibleType); - pw.print(", visibleView "); - if (view != null) { - pw.print(" visibility: " + view.getVisibility()); - pw.print(", alpha: " + view.getAlpha()); - pw.print(", clipBounds: " + view.getClipBounds()); - } else { - pw.print("null"); - } - pw.println(); + pw.println(", currentVisibleType: " + mVisibleType); + DumpUtilsKt.withIncreasedIndent(pw, () -> { + int[] visTypes = { + VISIBLE_TYPE_CONTRACTED, + VISIBLE_TYPE_EXPANDED, + VISIBLE_TYPE_HEADSUP, + VISIBLE_TYPE_SINGLELINE + }; + for (int visType : visTypes) { + pw.print("visType: " + visType + " :: "); + View view = getViewForVisibleType(visType); + if (view != null) { + pw.print("visibility: " + view.getVisibility()); + pw.print(", alpha: " + view.getAlpha()); + pw.print(", clipBounds: " + view.getClipBounds()); + } else { + pw.print("null"); + } + pw.println(); + } + }); } /** Add any existing SmartReplyView to the dump */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java index 94c5507cfbf4..98d43539841d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java @@ -16,8 +16,9 @@ package com.android.systemui.statusbar.notification.row; -import android.service.notification.NotificationListenerService; +import androidx.annotation.NonNull; +import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason; import com.android.systemui.statusbar.notification.collection.NotificationEntry; /** @@ -26,29 +27,23 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; public interface OnUserInteractionCallback { /** - * Handle a user interaction that triggers a notification dismissal. Called when a user clicks - * on an auto-cancelled notification or manually swipes to dismiss the notification. - * - * @param entry notification being dismissed - * @param cancellationReason reason for the cancellation - * @param groupSummaryToDismiss group summary to dismiss with `entry`. - */ - void onDismiss( - NotificationEntry entry, - @NotificationListenerService.NotificationCancelReason int cancellationReason, - NotificationEntry groupSummaryToDismiss); - - /** * Triggered after a user has changed the importance of the notification via its * {@link NotificationGuts}. */ void onImportanceChanged(NotificationEntry entry); - /** - * @param entry being dismissed by the user - * @return group summary that should be dismissed along with `entry`. Can be null if no - * relevant group summary exists or the group summary should not be dismissed with `entry`. + * Called once it is known that a dismissal will take place for the given reason. + * This returns a Runnable which MUST be invoked when the dismissal is ready to be completed. + * + * Registering for future dismissal is typically done before notifying the NMS that a + * notification was clicked or dismissed, but the local dismissal may happen later. + * + * @param entry the entry being cancelled + * @param cancellationReason the reason for the cancellation + * @return the runnable to call when the dismissal can happen */ - NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry); + @NonNull + Runnable registerFutureDismissal(@NonNull NotificationEntry entry, + @CancellationReason int cancellationReason); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index f26ecc32821d..a52f638e7c26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -102,8 +102,9 @@ public final class RowContentBindParams { * @see InflationFlag */ public void markContentViewsFreeable(@InflationFlag int contentViews) { + @InflationFlag int existingContentViews = contentViews &= mContentViews; mContentViews &= ~contentViews; - mDirtyContentViews &= ~contentViews; + mDirtyContentViews |= existingContentViews; } public @InflationFlag int getContentViews() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java index aa3e027fe1af..7414bdc13672 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row; +import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.util.AttributeSet; @@ -25,6 +26,8 @@ import android.view.animation.Interpolator; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.animation.Interpolators; +import java.util.function.Consumer; + /** * A common base class for all views in the notification stack scroller which don't have a * background. @@ -48,7 +51,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { }; private boolean mSecondaryAnimating = false; - private final Runnable mSecondaryVisibilityEndRunnable = () -> { + private final Consumer<Boolean> mSecondaryVisibilityEndRunnable = (cancelled) -> { mSecondaryAnimating = false; // If we were on screen, become GONE to avoid touches if (mSecondaryView == null) return; @@ -96,18 +99,20 @@ public abstract class StackScrollerDecorView extends ExpandableView { * @param animate True if we should fade to new visibility * @param runAfter Runnable to run after visibility updates */ - public void setContentVisible(boolean visible, boolean animate, Runnable runAfter) { + public void setContentVisible(boolean visible, boolean animate, Consumer<Boolean> runAfter) { if (mContentVisible != visible) { mContentAnimating = animate; mContentVisible = visible; - Runnable endRunnable = runAfter == null ? mContentVisibilityEndRunnable : () -> { + Consumer<Boolean> endRunnable = (cancelled) -> { mContentVisibilityEndRunnable.run(); - runAfter.run(); + if (runAfter != null) { + runAfter.accept(cancelled); + } }; setViewVisible(mContent, visible, animate, endRunnable); } else if (runAfter != null) { // Execute the runAfter runnable immediately if there's no animation to perform. - runAfter.run(); + runAfter.accept(true); } if (!mContentAnimating) { @@ -119,10 +124,6 @@ public abstract class StackScrollerDecorView extends ExpandableView { return mContentVisible; } - public void setVisible(boolean nowVisible, boolean animate) { - setVisible(nowVisible, animate, null); - } - /** * Make this view visible. If {@code false} is passed, the view will fade out it's content * and set the view Visibility to GONE. If only the content should be changed @@ -131,7 +132,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { * @param nowVisible should the view be visible * @param animate should the change be animated. */ - public void setVisible(boolean nowVisible, boolean animate, Runnable runAfter) { + public void setVisible(boolean nowVisible, boolean animate) { if (mIsVisible != nowVisible) { mIsVisible = nowVisible; if (animate) { @@ -142,10 +143,10 @@ public abstract class StackScrollerDecorView extends ExpandableView { } else { setWillBeGone(true); } - setContentVisible(nowVisible, true /* animate */, runAfter); + setContentVisible(nowVisible, true /* animate */, null /* runAfter */); } else { setVisibility(nowVisible ? VISIBLE : GONE); - setContentVisible(nowVisible, false /* animate */, runAfter); + setContentVisible(nowVisible, false /* animate */, null /* runAfter */); setWillBeGone(false); notifyHeightChanged(false /* needsAnimation */); } @@ -166,7 +167,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { } if (!mSecondaryAnimating) { - mSecondaryVisibilityEndRunnable.run(); + mSecondaryVisibilityEndRunnable.accept(true /* cancelled */); } } @@ -195,7 +196,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { * @param endRunnable A runnable that is run when the animation is done. */ private void setViewVisible(View view, boolean nowVisible, - boolean animate, Runnable endRunnable) { + boolean animate, Consumer<Boolean> endRunnable) { if (view == null) { return; } @@ -211,7 +212,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { if (!animate) { view.setAlpha(endValue); if (endRunnable != null) { - endRunnable.run(); + endRunnable.accept(true); } return; } @@ -222,7 +223,19 @@ public abstract class StackScrollerDecorView extends ExpandableView { .alpha(endValue) .setInterpolator(interpolator) .setDuration(mDuration) - .withEndAction(endRunnable); + .setListener(new AnimatorListenerAdapter() { + boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + endRunnable.accept(mCancelled); + } + }); } @Override @@ -231,7 +244,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) { // TODO: Use duration - setContentVisible(false, true /* animate */, onFinishedRunnable); + setContentVisible(false, true /* animate */, (cancelled) -> onFinishedRunnable.run()); return 0; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 3ea5e5b753a3..4d325e154a4f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1805,13 +1805,20 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) - public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) { + public void dismissViewAnimated( + View child, Consumer<Boolean> endRunnable, int delay, long duration) { if (child instanceof SectionHeaderView) { ((StackScrollerDecorView) child).setContentVisible( false /* visible */, true /* animate */, endRunnable); return; } - mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration, + mSwipeHelper.dismissChild( + child, + 0 /* velocity */, + endRunnable, + delay, + true /* useAccelerateInterpolator */, + duration, true /* isClearAll */); } @@ -5196,11 +5203,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (mClearAllListener != null) { mClearAllListener.onClearAll(selection); } - final Runnable dismissInBackend = () -> { - onClearAllAnimationsEnd(rowsToDismissInBackend, selection); + final Consumer<Boolean> dismissInBackend = (cancelled) -> { + if (cancelled) { + post(() -> onClearAllAnimationsEnd(rowsToDismissInBackend, selection)); + } else { + onClearAllAnimationsEnd(rowsToDismissInBackend, selection); + } }; if (viewsToAnimateAway.isEmpty()) { - dismissInBackend.run(); + dismissInBackend.accept(true); return; } // Disable normal animations @@ -5215,7 +5226,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable final int numItems = viewsToAnimateAway.size(); for (int i = numItems - 1; i >= 0; i--) { View view = viewsToAnimateAway.get(i); - Runnable endRunnable = null; + Consumer<Boolean> endRunnable = null; if (i == 0) { endRunnable = dismissInBackend; } @@ -5517,6 +5528,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ public void setFractionToShade(float fraction) { mAmbientState.setFractionToShade(fraction); + updateContentHeight(); // Recompute stack height with different section gap. requestChildrenUpdate(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 2493ccbe5a48..6a8f4796813a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -216,6 +216,9 @@ public class NotificationStackScrollLayoutController { mBarState = mStatusBarStateController.getState(); mStatusBarStateController.addCallback( mStateListener, SysuiStatusBarStateController.RANK_STACK_SCROLLER); + mLockscreenUserManager.addOnNeedsRedactionInPublicChangedListener( + mOnNeedsRedactionInPublicChangedListener); + updateClearButtonVisibility(); } @Override @@ -223,6 +226,8 @@ public class NotificationStackScrollLayoutController { mConfigurationController.removeCallback(mConfigurationListener); mZenModeController.removeCallback(mZenModeControllerCallback); mStatusBarStateController.removeCallback(mStateListener); + mLockscreenUserManager.removeOnNeedsRedactionInPublicChangedListener( + mOnNeedsRedactionInPublicChangedListener); } }; @@ -326,6 +331,7 @@ public class NotificationStackScrollLayoutController { mLockscreenUserManager.isAnyProfilePublicMode()); mView.onStatePostChange(mStatusBarStateController.fromShadeLocked()); mNotificationEntryManager.updateNotifications("CentralSurfaces state changed"); + updateClearButtonVisibility(); } }; @@ -338,6 +344,17 @@ public class NotificationStackScrollLayoutController { } }; + private final Runnable mOnNeedsRedactionInPublicChangedListener = new Runnable() { + @Override + public void run() { + // Whether or not the notification needs redaction when in public has changed, but if + // we're not actually in public, then we don't need to update anything. + if (mLockscreenUserManager.isAnyProfilePublicMode()) { + updateClearButtonVisibility(); + } + } + }; + /** * Set the overexpansion of the panel to be applied to the view. */ @@ -1274,12 +1291,44 @@ public class NotificationStackScrollLayoutController { return hasNotifications(selection, true /* clearable */); } + private boolean hasRedactedClearableSilentNotifs() { + if (!mLockscreenUserManager.isAnyProfilePublicMode()) { + return false; + } + for (int userId : mNotifStats.getClearableSilentSensitiveNotifUsers()) { + if (mLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(userId)) { + return true; + } + } + return false; + } + + private boolean hasClearableSilentNotifs() { + return mNotifStats.getHasClearableSilentNotifs() && !hasRedactedClearableSilentNotifs(); + } + + private boolean hasRedactedClearableAlertingNotifs() { + if (!mLockscreenUserManager.isAnyProfilePublicMode()) { + return false; + } + for (int userId : mNotifStats.getClearableAlertingSensitiveNotifUsers()) { + if (mLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(userId)) { + return true; + } + } + return false; + } + + private boolean hasClearableAlertingNotifs() { + return mNotifStats.getHasClearableAlertingNotifs() && !hasRedactedClearableAlertingNotifs(); + } + public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) { boolean hasAlertingMatchingClearable = isClearable - ? mNotifStats.getHasClearableAlertingNotifs() + ? hasClearableAlertingNotifs() : mNotifStats.getHasNonClearableAlertingNotifs(); boolean hasSilentMatchingClearable = isClearable - ? mNotifStats.getHasClearableSilentNotifs() + ? hasClearableSilentNotifs() : mNotifStats.getHasNonClearableSilentNotifs(); switch (selection) { case ROWS_GENTLE: @@ -1579,6 +1628,15 @@ public class NotificationStackScrollLayoutController { mNotificationActivityStarter = activityStarter; } + private void updateClearButtonVisibility() { + updateClearSilentButton(); + updateFooter(); + } + + private void updateClearSilentButton() { + mSilentHeaderController.setClearSectionEnabled(hasClearableSilentNotifs()); + } + /** * Enum for UiEvent logged from this class */ @@ -1904,6 +1962,7 @@ public class NotificationStackScrollLayoutController { @Override public void setNotifStats(@NonNull NotifStats notifStats) { mNotifStats = notifStats; + updateClearSilentButton(); updateFooter(); updateShowEmptyShadeView(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 2b79986662f1..799fee5e865d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -49,9 +49,10 @@ public class StackScrollAlgorithm { public static final float START_FRACTION = 0.5f; - private static final String LOG_TAG = "StackScrollAlgorithm"; - private final ViewGroup mHostView; + private static final String TAG = "StackScrollAlgorithm"; + private static final Boolean DEBUG = false; + private final ViewGroup mHostView; private int mPaddingBetweenElements; private int mGapHeight; private int mGapHeightOnLockscreen; @@ -126,6 +127,37 @@ public class StackScrollAlgorithm { return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState); } + private void log(String s) { + if (DEBUG) { + android.util.Log.i(TAG, s); + } + } + + public void logView(View view, String s) { + String viewString = ""; + if (view instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = ((ExpandableNotificationRow) view); + if (row.getEntry() == null) { + viewString = "ExpandableNotificationRow has null NotificationEntry"; + } else { + viewString = row.getEntry().getSbn().getId() + ""; + } + } else if (view == null) { + viewString = "View is null"; + } else if (view instanceof SectionHeaderView) { + viewString = "SectionHeaderView"; + } else if (view instanceof FooterView) { + viewString = "FooterView"; + } else if (view instanceof MediaContainerView) { + viewString = "MediaContainerView"; + } else if (view instanceof EmptyShadeView) { + viewString = "EmptyShadeView"; + } else { + viewString = view.toString(); + } + log(viewString + " " + s); + } + private void resetChildViewStates() { int numChildren = mHostView.getChildCount(); for (int i = 0; i < numChildren; i++) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java index 9863a0ed1ce0..f386797e322a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java @@ -29,6 +29,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.HeadsUpStatusBarView; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -40,8 +41,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.util.ViewController; -import java.util.Optional; import java.util.ArrayList; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -61,17 +62,17 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar private final NotificationIconAreaController mNotificationIconAreaController; private final HeadsUpManagerPhone mHeadsUpManager; private final NotificationStackScrollLayoutController mStackScrollerController; - private final DarkIconDispatcher mDarkIconDispatcher; private final NotificationPanelViewController mNotificationPanelViewController; - private final Consumer<ExpandableNotificationRow> - mSetTrackingHeadsUp = this::setTrackingHeadsUp; + private final Consumer<ExpandableNotificationRow> mSetTrackingHeadsUp = + this::setTrackingHeadsUp; private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction; private final KeyguardBypassController mBypassController; private final StatusBarStateController mStatusBarStateController; private final CommandQueue mCommandQueue; private final NotificationWakeUpCoordinator mWakeUpCoordinator; - + private final NotificationLockscreenUserManager mNotifLockscreenUserManager; + private final Runnable mRedactionChanged = this::updateRedaction; private final View mClockView; private final Optional<View> mOperatorNameViewOptional; @@ -90,6 +91,13 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar }; private boolean mAnimationsEnabled = true; private final KeyguardStateController mKeyguardStateController; + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStatePostChange() { + updateRedaction(); + } + }; @VisibleForTesting @Inject @@ -98,6 +106,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar HeadsUpManagerPhone headsUpManager, StatusBarStateController stateController, KeyguardBypassController bypassController, + NotificationLockscreenUserManager notifLockscreenUserManager, NotificationWakeUpCoordinator wakeUpCoordinator, DarkIconDispatcher darkIconDispatcher, KeyguardStateController keyguardStateController, @@ -125,6 +134,7 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar mClockView = clockView; mOperatorNameViewOptional = operatorNameViewOptional; mDarkIconDispatcher = darkIconDispatcher; + mNotifLockscreenUserManager = notifLockscreenUserManager; mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override @@ -156,6 +166,8 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar mNotificationPanelViewController.setHeadsUpAppearanceController(this); mStackScrollerController.addOnExpandedHeightChangedListener(mSetExpandedHeight); mDarkIconDispatcher.addDarkReceiver(this); + mNotifLockscreenUserManager.addOnNeedsRedactionInPublicChangedListener(mRedactionChanged); + mStatusBarStateController.addCallback(mStatusBarStateListener); } @Override @@ -167,6 +179,9 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar mNotificationPanelViewController.setHeadsUpAppearanceController(null); mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight); mDarkIconDispatcher.removeDarkReceiver(this); + mNotifLockscreenUserManager + .removeOnNeedsRedactionInPublicChangedListener(mRedactionChanged); + mStatusBarStateController.removeCallback(mStatusBarStateListener); } private void updateIsolatedIconLocation(boolean requireStateUpdate) { @@ -180,6 +195,19 @@ public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBar updateHeader(entry); } + private void updateRedaction() { + NotificationEntry showingEntry = mView.getShowingEntry(); + if (showingEntry == null) { + return; + } + int notifUserId = showingEntry.getSbn().getUserId(); + boolean redactSensitiveContent = + mNotifLockscreenUserManager.isLockscreenPublicMode(notifUserId) + && mNotifLockscreenUserManager + .sensitiveNotifsNeedRedactionInPublic(notifUserId); + mView.setRedactSensitiveContent(redactSensitiveContent); + } + private void updateTopEntry() { NotificationEntry newEntry = null; if (shouldBeVisible()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index 347e05cc7f75..fd307df8d304 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -1198,15 +1198,19 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL if (tileIcon != null) { mWalletButton.setImageDrawable(tileIcon); } - updateWalletVisibility(); - updateAffordanceColors(); + post(() -> { + updateWalletVisibility(); + updateAffordanceColors(); + }); } @Override public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) { mHasCard = false; - updateWalletVisibility(); - updateAffordanceColors(); + post(() -> { + updateWalletVisibility(); + updateAffordanceColors(); + }); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 0b721383e2d1..8792118fb9ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -188,6 +188,10 @@ public class KeyguardBouncer { } if (mContainer.getVisibility() == View.VISIBLE || mShowingSoon) { + // Calls to reset must resume the ViewControllers when in fullscreen mode + if (needsFullscreenBouncer()) { + mKeyguardViewController.onResume(); + } return; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt deleted file mode 100644 index f46d07338206..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.systemui.statusbar.phone - -import com.android.systemui.statusbar.notification.collection.NotificationEntry - -/** Provides events about [android.app.Activity] launches from Notifications. */ -interface NotifActivityLaunchEvents { - - /** Registers a [Listener] to be invoked when notification activity launch events occur. */ - fun registerListener(listener: Listener) - - /** Unregisters a [Listener] previously registered via [registerListener] */ - fun unregisterListener(listener: Listener) - - /** Listener for events about [android.app.Activity] launches from Notifications. */ - interface Listener { - - /** Invoked when an activity has started launching from a notification. */ - fun onStartLaunchNotifActivity(entry: NotificationEntry) - - /** Invoked when an activity has finished launching. */ - fun onFinishLaunchNotifActivity(entry: NotificationEntry) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index ec13b1f835e8..314ab2221b19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -32,7 +32,6 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_N import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; -import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD; import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED; import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN; @@ -1693,9 +1692,11 @@ public class NotificationPanelViewController extends PanelViewController { if (mShouldUseSplitNotificationShade && isOnKeyguard()) { // It's a special case as this method is likely to not be initiated by finger movement // but rather called from adb shell or accessibility service. - // In the future method below could be used for non-split shade as well but currently - // motion in that case looks worse than using flingSettings. - // TODO: make below function transitioning smoothly also in portrait with empty target + // We're using LockscreenShadeTransitionController because on lockscreen that's the + // source of truth for all shade motion. Not using it would make part of state to be + // outdated and will cause bugs. Ideally we'd use this controller also for non-split + // case but currently motion in portrait looks worse than when using flingSettings. + // TODO: make below function transitioning smoothly also in portrait with null target mLockscreenShadeTransitionController.goToLockedShade( /* expandedView= */null, /* needsQSAnimation= */false); } else if (isFullyCollapsed()) { @@ -2363,9 +2364,12 @@ public class NotificationPanelViewController extends PanelViewController { mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY); setQSClippingBounds(); - // Only need to notify the notification stack when we're not in split screen mode. If we - // do, then the notification panel starts scrolling along with the QS. - if (!mShouldUseSplitNotificationShade) { + if (mShouldUseSplitNotificationShade) { + // In split shade we want to pretend that QS are always collapsed so their behaviour and + // interactions don't influence notifications as they do in portrait. But we want to set + // 0 explicitly in case we're rotating from non-split shade with QS expansion of 1. + mNotificationStackScrollLayoutController.setQsExpansionFraction(0); + } else { mNotificationStackScrollLayoutController.setQsExpansionFraction(qsExpansionFraction); } @@ -3373,9 +3377,9 @@ public class NotificationPanelViewController extends PanelViewController { @Override public void setIsLaunchAnimationRunning(boolean running) { - boolean wasRunning = isLaunchTransitionRunning(); + boolean wasRunning = mIsLaunchAnimationRunning; super.setIsLaunchAnimationRunning(running); - if (wasRunning != isLaunchTransitionRunning()) { + if (wasRunning != mIsLaunchAnimationRunning) { mPanelEventsEmitter.notifyLaunchingActivityChanged(running); } } @@ -3939,10 +3943,6 @@ public class NotificationPanelViewController extends PanelViewController { } } - public boolean hasActiveClearableNotifications() { - return mNotificationStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL); - } - public RemoteInputController.Delegate createRemoteInputDelegate() { return mNotificationStackScrollLayoutController.createDelegate(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index f9e17da9773f..0e77866bef1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -402,6 +402,8 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mScrimBehind.setFocusable(!state.isLowPowerState()); mNotificationsScrim.setFocusable(!state.isLowPowerState()); + mScrimInFront.setBlendWithMainColor(state.shouldBlendWithMainColor()); + // Cancel blanking transitions that were pending before we requested a new state if (mPendingFrameCallback != null) { mScrimBehind.removeCallbacks(mPendingFrameCallback); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 47b705845fce..4a5f712d587c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -203,6 +203,11 @@ public enum ScrimState { public boolean isLowPowerState() { return true; } + + @Override + public boolean shouldBlendWithMainColor() { + return false; + } }, /** @@ -325,6 +330,13 @@ public enum ScrimState { public void prepare(ScrimState previousState) { } + /** + * Whether a particular state should enable blending with extracted theme colors. + */ + public boolean shouldBlendWithMainColor() { + return true; + } + public float getFrontAlpha() { return mFrontAlpha; } @@ -422,4 +434,4 @@ public enum ScrimState { public void setClipQsScrim(boolean clipsQsScrim) { mClipQsScrim = clipsQsScrim; } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt index 56b6dfc42ee3..c0922163903f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt @@ -20,7 +20,9 @@ class StatusBarLaunchAnimatorController( override fun onIntentStarted(willAnimate: Boolean) { delegate.onIntentStarted(willAnimate) - if (!willAnimate) { + if (willAnimate) { + centralSurfaces.notificationPanelViewController.setIsLaunchAnimationRunning(true) + } else { centralSurfaces.collapsePanelOnMainThread() } } @@ -51,6 +53,7 @@ class StatusBarLaunchAnimatorController( override fun onLaunchAnimationCancelled() { delegate.onLaunchAnimationCancelled() + centralSurfaces.notificationPanelViewController.setIsLaunchAnimationRunning(false) centralSurfaces.onLaunchAnimationCancelled(isLaunchForActivity) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 87ca942edff2..cf776e3b60d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -41,7 +41,6 @@ import android.text.TextUtils; import android.util.EventLog; import android.view.View; -import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; @@ -52,7 +51,6 @@ import com.android.systemui.ActivityIntentHelper; import com.android.systemui.EventLogTags; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.assist.AssistManager; -import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; @@ -77,7 +75,6 @@ import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.policy.HeadsUpUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.ListenerSet; import com.android.systemui.wmshell.BubblesManager; import java.util.Optional; @@ -131,7 +128,6 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte private final ActivityLaunchAnimator mActivityLaunchAnimator; private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider; private final OnUserInteractionCallback mOnUserInteractionCallback; - private final LaunchEventsEmitter mLaunchEventsEmitter; private boolean mIsCollapsingToShowActivityOverLockscreen; @@ -170,8 +166,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte NotificationPresenter presenter, NotificationPanelViewController panel, ActivityLaunchAnimator activityLaunchAnimator, - NotificationLaunchAnimatorControllerProvider notificationAnimationProvider, - LaunchEventsEmitter launchEventsEmitter) { + NotificationLaunchAnimatorControllerProvider notificationAnimationProvider) { mContext = context; mCommandQueue = commandQueue; mMainThreadHandler = mainThreadHandler; @@ -207,7 +202,6 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte mNotificationPanel = panel; mActivityLaunchAnimator = activityLaunchAnimator; mNotificationAnimationProvider = notificationAnimationProvider; - mLaunchEventsEmitter = launchEventsEmitter; if (!mNotifPipelineFlags.isNewPipelineEnabled()) { mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @@ -229,14 +223,13 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte /** * Called when a notification is clicked. * - * @param sbn notification that was clicked + * @param entry notification that was clicked * @param row row for that notification */ @Override - public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) { - mLogger.logStartingActivityFromClick(sbn.getKey()); + public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) { + mLogger.logStartingActivityFromClick(entry); - final NotificationEntry entry = row.getEntry(); if (mRemoteInputManager.isRemoteInputActive(entry) && !TextUtils.isEmpty(row.getActiveRemoteInputText())) { // We have an active remote input typed and the user clicked on the notification. @@ -244,7 +237,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte mRemoteInputManager.closeRemoteInputs(); return; } - Notification notification = sbn.getNotification(); + Notification notification = entry.getSbn().getNotification(); final PendingIntent intent = notification.contentIntent != null ? notification.contentIntent : notification.fullScreenIntent; @@ -254,12 +247,10 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte // The only valid case is Bubble notifications. Guard against other cases // entering here. if (intent == null && !isBubble) { - mLogger.logNonClickableNotification(sbn.getKey()); + mLogger.logNonClickableNotification(entry); return; } - mLaunchEventsEmitter.notifyStartLaunchNotifActivity(entry); - boolean isActivityIntent = intent != null && intent.isActivity() && !isBubble; final boolean willLaunchResolverActivity = isActivityIntent && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(), @@ -287,7 +278,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte } else { mActivityStarter.dismissKeyguardThenExecute( postKeyguardAction, - () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry), + null, willLaunchResolverActivity); } } @@ -299,7 +290,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte boolean isActivityIntent, boolean animate, boolean showOverLockscreen) { - mLogger.logHandleClickAfterKeyguardDismissed(entry.getKey()); + mLogger.logHandleClickAfterKeyguardDismissed(entry); final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed( entry, row, intent, isActivityIntent, animate); @@ -326,7 +317,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte boolean isActivityIntent, boolean animate) { String notificationKey = entry.getKey(); - mLogger.logHandleClickAfterPanelCollapsed(notificationKey); + mLogger.logHandleClickAfterPanelCollapsed(entry); try { // The intent we are sending is for the application, which @@ -367,11 +358,9 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte } final boolean canBubble = entry.canBubble(); if (canBubble) { - mLogger.logExpandingBubble(notificationKey); + mLogger.logExpandingBubble(entry); removeHunAfterClick(row); expandBubbleStackOnMainThread(entry); - mMainThreadHandler.post( - () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry)); } else { startNotificationIntent(intent, fillInIntent, entry, row, animate, isActivityIntent); } @@ -381,30 +370,13 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); - // retrieve the group summary to remove with this entry before we tell NMS the - // notification was clicked to avoid a race condition - final boolean shouldAutoCancel = shouldAutoCancel(entry.getSbn()); - final NotificationEntry summaryToRemove = shouldAutoCancel - ? mOnUserInteractionCallback.getGroupSummaryToDismiss(entry) : null; - - // inform NMS that the notification was clicked - mClickNotifier.onNotificationClick(notificationKey, nv); - - if (!canBubble && (shouldAutoCancel + if (!canBubble && (shouldAutoCancel(entry.getSbn()) || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(notificationKey))) { + final Runnable removeNotification = + mOnUserInteractionCallback.registerFutureDismissal(entry, REASON_CLICK); // Immediately remove notification from visually showing. // We have to post the removal to the UI thread for synchronization. mMainThreadHandler.post(() -> { - final Runnable removeNotification = () -> { - mOnUserInteractionCallback.onDismiss(entry, REASON_CLICK, summaryToRemove); - if (!animate) { - // If we're animating, this would be invoked after the activity launch - // animation completes. Since we're not animating, the launch already - // happened synchronously, so we notify the launch is complete here after - // onDismiss. - mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry); - } - }; if (mPresenter.isCollapsing()) { // To avoid lags we're only performing the remove after the shade is collapsed mShadeController.addPostCollapseAction(removeNotification); @@ -412,12 +384,11 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte removeNotification.run(); } }); - } else if (!canBubble && !animate) { - // Not animating, this is the end of the launch flow (see above comment for more info). - mMainThreadHandler.post( - () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry)); } + // inform NMS that the notification was clicked + mClickNotifier.onNotificationClick(notificationKey, nv); + mIsCollapsingToShowActivityOverLockscreen = false; } @@ -434,24 +405,14 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte // will focus follow operation only after drag-and-drop that notification. final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); - // retrieve the group summary to remove with this entry before we tell NMS the - // notification was clicked to avoid a race condition - final boolean shouldAutoCancel = shouldAutoCancel(entry.getSbn()); - final NotificationEntry summaryToRemove = shouldAutoCancel - ? mOnUserInteractionCallback.getGroupSummaryToDismiss(entry) : null; - String notificationKey = entry.getKey(); - // inform NMS that the notification was clicked - mClickNotifier.onNotificationClick(notificationKey, nv); - - if (shouldAutoCancel || mRemoteInputManager.isNotificationKeptForRemoteInputHistory( - notificationKey)) { + if (shouldAutoCancel(entry.getSbn()) + || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(notificationKey)) { + final Runnable removeNotification = + mOnUserInteractionCallback.registerFutureDismissal(entry, REASON_CLICK); // Immediately remove notification from visually showing. // We have to post the removal to the UI thread for synchronization. mMainThreadHandler.post(() -> { - final Runnable removeNotification = () -> - mOnUserInteractionCallback.onDismiss( - entry, REASON_CLICK, summaryToRemove); if (mPresenter.isCollapsing()) { // To avoid lags we're only performing the remove // after the shade is collapsed @@ -462,6 +423,9 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte }); } + // inform NMS that the notification was clicked + mClickNotifier.onNotificationClick(notificationKey, nv); + mIsCollapsingToShowActivityOverLockscreen = false; } @@ -489,15 +453,11 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte ExpandableNotificationRow row, boolean animate, boolean isActivityIntent) { - mLogger.logStartNotificationIntent(entry.getKey()); + mLogger.logStartNotificationIntent(entry); try { - Runnable onFinishAnimationCallback = animate - ? () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry) - : null; ActivityLaunchAnimator.Controller animationController = new StatusBarLaunchAnimatorController( - mNotificationAnimationProvider - .getAnimatorController(row, onFinishAnimationCallback), + mNotificationAnimationProvider.getAnimatorController(row, null), mCentralSurfaces, isActivityIntent); mActivityLaunchAnimator.startPendingIntentWithAnimation( @@ -515,7 +475,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte : getActivityOptions(mCentralSurfaces.getDisplayId(), adapter); int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null, null, null, options); - mLogger.logSendPendingIntent(entry.getKey(), intent, result); + mLogger.logSendPendingIntent(entry, intent, result); return result; }); } catch (PendingIntent.CanceledException e) { @@ -622,9 +582,9 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte void handleFullScreenIntent(NotificationEntry entry) { if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) { if (shouldSuppressFullScreenIntent(entry)) { - mLogger.logFullScreenIntentSuppressedByDnD(entry.getKey()); + mLogger.logFullScreenIntentSuppressedByDnD(entry); } else if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) { - mLogger.logFullScreenIntentNotImportantEnough(entry.getKey()); + mLogger.logFullScreenIntentNotImportantEnough(entry); } else { // Stop screensaver if the notification has a fullscreen intent. // (like an incoming phone call) @@ -639,7 +599,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte // not immersive & a fullscreen alert should be shown final PendingIntent fullscreenIntent = entry.getSbn().getNotification().fullScreenIntent; - mLogger.logSendingFullScreenIntent(entry.getKey(), fullscreenIntent); + mLogger.logSendingFullScreenIntent(entry, fullscreenIntent); try { EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, entry.getKey()); @@ -685,35 +645,4 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte return entry.shouldSuppressFullScreenIntent(); } - - @SysUISingleton - static class LaunchEventsEmitter implements NotifActivityLaunchEvents { - - private final ListenerSet<Listener> mListeners = new ListenerSet<>(); - - @Inject - LaunchEventsEmitter() {} - - @Override - public void registerListener(@NonNull Listener listener) { - mListeners.addIfAbsent(listener); - } - - @Override - public void unregisterListener(@NonNull Listener listener) { - mListeners.remove(listener); - } - - private void notifyStartLaunchNotifActivity(NotificationEntry entry) { - for (Listener listener : mListeners) { - listener.onStartLaunchNotifActivity(entry); - } - } - - private void notifyFinishLaunchNotifActivity(NotificationEntry entry) { - for (Listener listener : mListeners) { - listener.onFinishLaunchNotifActivity(entry); - } - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt index 2fbe520a4b61..b9a1413ff791 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt @@ -23,46 +23,48 @@ import com.android.systemui.log.LogLevel.ERROR import com.android.systemui.log.LogLevel.INFO import com.android.systemui.log.LogLevel.WARNING import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey import javax.inject.Inject class StatusBarNotificationActivityStarterLogger @Inject constructor( @NotifInteractionLog private val buffer: LogBuffer ) { - fun logStartingActivityFromClick(key: String) { + fun logStartingActivityFromClick(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = key + str1 = entry.logKey }, { "(1/5) onNotificationClicked: $str1" }) } - fun logHandleClickAfterKeyguardDismissed(key: String) { + fun logHandleClickAfterKeyguardDismissed(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = key + str1 = entry.logKey }, { "(2/5) handleNotificationClickAfterKeyguardDismissed: $str1" }) } - fun logHandleClickAfterPanelCollapsed(key: String) { + fun logHandleClickAfterPanelCollapsed(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = key + str1 = entry.logKey }, { "(3/5) handleNotificationClickAfterPanelCollapsed: $str1" }) } - fun logStartNotificationIntent(key: String) { + fun logStartNotificationIntent(entry: NotificationEntry) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey }, { "(4/5) startNotificationIntent: $str1" }) } - fun logSendPendingIntent(key: String, pendingIntent: PendingIntent, result: Int) { + fun logSendPendingIntent(entry: NotificationEntry, pendingIntent: PendingIntent, result: Int) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey str2 = pendingIntent.intent.toString() int1 = result }, { @@ -70,9 +72,9 @@ class StatusBarNotificationActivityStarterLogger @Inject constructor( }) } - fun logExpandingBubble(key: String) { + fun logExpandingBubble(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = key + str1 = entry.logKey }, { "Expanding bubble for $str1 (rather than firing intent)" }) @@ -86,33 +88,33 @@ class StatusBarNotificationActivityStarterLogger @Inject constructor( }) } - fun logNonClickableNotification(key: String) { + fun logNonClickableNotification(entry: NotificationEntry) { buffer.log(TAG, ERROR, { - str1 = key + str1 = entry.logKey }, { "onNotificationClicked called for non-clickable notification! $str1" }) } - fun logFullScreenIntentSuppressedByDnD(key: String) { + fun logFullScreenIntentSuppressedByDnD(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = key + str1 = entry.logKey }, { "No Fullscreen intent: suppressed by DND: $str1" }) } - fun logFullScreenIntentNotImportantEnough(key: String) { + fun logFullScreenIntentNotImportantEnough(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { - str1 = key + str1 = entry.logKey }, { "No Fullscreen intent: not important enough: $str1" }) } - fun logSendingFullScreenIntent(key: String, pendingIntent: PendingIntent) { + fun logSendingFullScreenIntent(entry: NotificationEntry, pendingIntent: PendingIntent) { buffer.log(TAG, INFO, { - str1 = key + str1 = entry.logKey str2 = pendingIntent.intent.toString() }, { "Notification $str1 has fullScreenIntent; sending fullScreenIntent $str2" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index aa061d74f6c6..037063f4f7b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -411,8 +411,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, if (nowExpanded) { if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { mShadeTransitionController.goToLockedShade(clickedEntry.getRow()); - } else if (clickedEntry.isSensitive() - && mDynamicPrivacyController.isInLockedDownShade()) { + } else if (mDynamicPrivacyController.isInLockedDownShade() + && mLockscreenUserManager.notifNeedsRedactionInPublic(clickedEntry)) { mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); mActivityStarter.dismissKeyguardThenExecute(() -> false /* dismissAction */ , null /* cancelRunnable */, false /* afterKeyguardGone */); @@ -480,7 +480,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, .isLockscreenPublicMode(mLockscreenUserManager.getCurrentUserId()); boolean userPublic = devicePublic || mLockscreenUserManager.isLockscreenPublicMode(sbn.getUserId()); - boolean needsRedaction = mLockscreenUserManager.needsRedaction(entry); + boolean needsRedaction = mLockscreenUserManager.notifNeedsRedactionInPublic(entry); if (userPublic && needsRedaction) { // TODO(b/135046837): we can probably relax this with dynamic privacy return true; diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 7920d388c670..c6a8445d2443 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -78,6 +78,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.PrintWriter; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -639,6 +640,11 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable { } private Style fetchThemeStyleFromSetting() { + // Allow-list of Style objects that can be created from a setting string, i.e. can be + // used as a system-wide theme. + // - Content intentionally excluded, intended for media player, not system-wide + List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT, + Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT); Style style = mThemeStyle; final String overlayPackageJson = mSecureSettings.getStringForUser( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, @@ -648,6 +654,9 @@ public class ThemeOverlayController extends CoreStartable implements Dumpable { JSONObject object = new JSONObject(overlayPackageJson); style = Style.valueOf( object.getString(ThemeOverlayApplier.OVERLAY_CATEGORY_THEME_STYLE)); + if (!validStyles.contains(style)) { + style = Style.TONAL_SPOT; + } } catch (JSONException | IllegalArgumentException e) { Log.i(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e); style = Style.TONAL_SPOT; diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java b/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java index 2b7a33260248..90f24347d20d 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java @@ -18,15 +18,18 @@ package com.android.systemui.tv; import android.app.Activity; import android.graphics.PixelFormat; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; +import android.view.View; import android.view.WindowManager; import com.android.systemui.R; +import java.util.Collections; import java.util.function.Consumer; /** @@ -75,6 +78,12 @@ public abstract class TvBottomSheetActivity extends Activity { getWindow().setElevation(getWindow().getElevation() + 5); getWindow().setBackgroundBlurRadius(getResources().getDimensionPixelSize( R.dimen.bottom_sheet_background_blur_radius)); + + final View rootView = findViewById(R.id.bottom_sheet); + rootView.addOnLayoutChangeListener((view, l, t, r, b, oldL, oldT, oldR, oldB) -> { + rootView.setUnrestrictedPreferKeepClearRects( + Collections.singletonList(new Rect(0, 0, r - l, b - t))); + }); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java index acff8712e92e..8c842f162e24 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java @@ -19,7 +19,6 @@ package com.android.systemui.wallet.controller; import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE; import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE; -import android.annotation.CallbackExecutor; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -65,7 +64,6 @@ public class QuickAccessWalletController { private static final long RECREATION_TIME_WINDOW = TimeUnit.MINUTES.toMillis(10L); private final Context mContext; private final Executor mExecutor; - private final Executor mCallbackExecutor; private final Executor mBgExecutor; private final SecureSettings mSecureSettings; private final SystemClock mClock; @@ -82,14 +80,12 @@ public class QuickAccessWalletController { public QuickAccessWalletController( Context context, @Main Executor executor, - @CallbackExecutor Executor callbackExecutor, @Background Executor bgExecutor, SecureSettings secureSettings, QuickAccessWalletClient quickAccessWalletClient, SystemClock clock) { mContext = context; mExecutor = executor; - mCallbackExecutor = callbackExecutor; mBgExecutor = bgExecutor; mSecureSettings = secureSettings; mQuickAccessWalletClient = quickAccessWalletClient; @@ -180,7 +176,7 @@ public class QuickAccessWalletController { int iconSizePx = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_icon_size); GetWalletCardsRequest request = new GetWalletCardsRequest(cardWidth, cardHeight, iconSizePx, /* maxCards= */ 1); - mQuickAccessWalletClient.getWalletCards(mExecutor, request, cardsRetriever); + mQuickAccessWalletClient.getWalletCards(mBgExecutor, request, cardsRetriever); } /** @@ -212,7 +208,7 @@ public class QuickAccessWalletController { public void startQuickAccessUiIntent(ActivityStarter activityStarter, ActivityLaunchAnimator.Controller animationController, boolean hasCard) { - mQuickAccessWalletClient.getWalletPendingIntent(mCallbackExecutor, + mQuickAccessWalletClient.getWalletPendingIntent(mBgExecutor, walletPendingIntent -> { if (walletPendingIntent != null) { startQuickAccessViaPendingIntent(walletPendingIntent, activityStarter, diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 7c211b22e657..57253af57f0e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -159,6 +159,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { when(mContext.getDisplay()).thenReturn(mDisplay); // Not support hwc layer by default doReturn(null).when(mDisplay).getDisplayDecorationSupport(); + doReturn(mDisplayMode).when(mDisplay).getMode(); when(mMockTypedArray.length()).thenReturn(0); mPrivacyDotTopLeftDecorProvider = spy(new PrivacyDotCornerDecorProviderImpl( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt index 2abc666e87fb..a4d223853b12 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt @@ -14,6 +14,8 @@ import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardViewController import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags +import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController +import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.KeyguardStateController @@ -52,6 +54,11 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { private lateinit var surfaceTransactionApplier: SyncRtSurfaceTransactionApplier @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController + @Mock + private lateinit var notificationShadeWindowController: NotificationShadeWindowController + + @Mock + private lateinit var launcherUnlockAnimationController: ILauncherUnlockAnimationController.Stub private lateinit var remoteAnimationTarget: RemoteAnimationTarget @@ -60,8 +67,11 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) keyguardUnlockAnimationController = KeyguardUnlockAnimationController( context, keyguardStateController, { keyguardViewMediator }, keyguardViewController, - featureFlags, { biometricUnlockController }, statusBarStateController + featureFlags, { biometricUnlockController }, statusBarStateController, + notificationShadeWindowController ) + keyguardUnlockAnimationController.setLauncherUnlockController( + launcherUnlockAnimationController) `when`(keyguardViewController.viewRootImpl).thenReturn(mock(ViewRootImpl::class.java)) @@ -194,4 +204,18 @@ class KeyguardUnlockAnimationControllerTest : SysuiTestCase() { assertTrue(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) assertFalse(keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.isRunning) } + + @Test + fun doNotPlayCannedUnlockAnimation_ifLaunchingApp() { + `when`(notificationShadeWindowController.isLaunchingActivity).thenReturn(true) + + keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation( + remoteAnimationTarget, + 0 /* startTime */, + true /* requestedShowSurfaceBehindKeyguard */ + ) + + assertFalse(keyguardUnlockAnimationController.canPerformInWindowLauncherAnimations()) + assertFalse(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java index c532ed5ab651..24d051508fde 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java @@ -135,8 +135,7 @@ public class LockIconViewControllerTest extends SysuiTestCase { .startMocking(); MockitoAnnotations.initMocks(this); - when(mLockIconView.getResources()).thenReturn(mResources); - when(mLockIconView.getContext()).thenReturn(mContext); + setupLockIconViewMocks(); when(mContext.getResources()).thenReturn(mResources); when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); Rect windowBounds = new Rect(0, 0, 800, 1200); @@ -206,13 +205,14 @@ public class LockIconViewControllerTest extends SysuiTestCase { } @Test - public void testUpdateFingerprintLocationOnAuthenticatorsRegistered() { + public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { // GIVEN fp sensor location is not available pre-init when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); mLockIconViewController.init(); captureAttachListener(); mAttachListener.onViewAttachedToWindow(mLockIconView); + resetLockIconView(); // reset any method call counts for when we verify method calls later // GIVEN fp sensor location is available post-attached captureAuthControllerCallback(); @@ -228,6 +228,29 @@ public class LockIconViewControllerTest extends SysuiTestCase { } @Test + public void testUpdateLockIconLocationOnUdfpsLocationChanged() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + mLockIconViewController.init(); + captureAttachListener(); + mAttachListener.onViewAttachedToWindow(mLockIconView); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, PointF> udfps = setupUdfps(); + + // WHEN udfps location changes + mAuthControllerCallback.onUdfpsLocationChanged(); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { // GIVEN Udpfs sensor location is available setupUdfps(); @@ -440,4 +463,14 @@ public class LockIconViewControllerTest extends SysuiTestCase { mKeyguardUpdateMonitorCallbackCaptor.capture()); mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); } + + private void setupLockIconViewMocks() { + when(mLockIconView.getResources()).thenReturn(mResources); + when(mLockIconView.getContext()).thenReturn(mContext); + } + + private void resetLockIconView() { + reset(mLockIconView); + setupLockIconViewMocks(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt index 0d917e3b19a8..ceb811b8d8aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt @@ -63,6 +63,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var falsingManager: FalsingManager @Mock lateinit var dumpManager: DumpManager @Mock lateinit var logger: MediaUiEventLogger + @Mock lateinit var debugLogger: MediaCarouselControllerLogger private val clock = FakeSystemClock() private lateinit var mediaCarouselController: MediaCarouselController @@ -83,7 +84,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { falsingCollector, falsingManager, dumpManager, - logger + logger, + debugLogger ) MediaPlayerData.clear() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt index 91c0cc2ff891..823d4ae8c447 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -23,6 +23,8 @@ import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture @@ -63,10 +65,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Mock private lateinit var mediaControllerFactory: MediaControllerFactory @Mock private lateinit var mediaController: MediaController @Mock private lateinit var logger: MediaTimeoutLogger + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController private lateinit var executor: FakeExecutor @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback> + @Captor private lateinit var dozingCallbackCaptor: + ArgumentCaptor<StatusBarStateController.StateListener> @JvmField @Rule val mockito = MockitoJUnit.rule() private lateinit var metadataBuilder: MediaMetadata.Builder private lateinit var playbackBuilder: PlaybackState.Builder @@ -74,12 +79,19 @@ class MediaTimeoutListenerTest : SysuiTestCase() { private lateinit var mediaData: MediaData private lateinit var resumeData: MediaData private lateinit var mediaTimeoutListener: MediaTimeoutListener + private var clock = FakeSystemClock() @Before fun setup() { `when`(mediaControllerFactory.create(any())).thenReturn(mediaController) - executor = FakeExecutor(FakeSystemClock()) - mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor, logger) + executor = FakeExecutor(clock) + mediaTimeoutListener = MediaTimeoutListener( + mediaControllerFactory, + executor, + logger, + statusBarStateController, + clock + ) mediaTimeoutListener.timeoutCallback = timeoutCallback mediaTimeoutListener.stateCallback = stateCallback @@ -530,6 +542,49 @@ class MediaTimeoutListenerTest : SysuiTestCase() { verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!)) } + @Test + fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() { + // When paused media is loaded + testOnMediaDataLoaded_registersPlaybackListener() + mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) + + // And we doze past the scheduled timeout + val time = clock.currentTimeMillis() + clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT) + assertThat(executor.numPending()).isEqualTo(1) + + // Then when no longer dozing, the timeout runs immediately + dozingCallbackCaptor.value.onDozingChanged(false) + verify(timeoutCallback).invoke(eq(KEY), eq(true)) + verify(logger).logTimeout(eq(KEY)) + + // and cancel any later scheduled timeout + verify(logger).logTimeoutCancelled(eq(KEY), any()) + assertThat(executor.numPending()).isEqualTo(0) + } + + @Test + fun testTimeoutCallback_dozeShortTime_notInvokedOnWakeup() { + // When paused media is loaded + val time = clock.currentTimeMillis() + clock.setElapsedRealtime(time) + testOnMediaDataLoaded_registersPlaybackListener() + mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) + + // And we doze, but not past the scheduled timeout + clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L) + assertThat(executor.numPending()).isEqualTo(1) + + // Then when no longer dozing, the timeout remains scheduled + dozingCallbackCaptor.value.onDozingChanged(false) + verify(timeoutCallback, never()).invoke(eq(KEY), eq(true)) + assertThat(executor.numPending()).isEqualTo(1) + } + private fun loadMediaDataWithPlaybackState(state: PlaybackState) { `when`(mediaController.playbackState).thenReturn(state) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java index 863484b62a00..890e4de118e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java @@ -133,7 +133,7 @@ public class ColorSchemeTest extends SysuiTestCase { Style.VIBRANT /* style */); int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); Cam cam = Cam.fromInt(neutralMid); - Assert.assertTrue("chroma was " + cam.getChroma(), Math.floor(cam.getChroma()) <= 10.0); + Assert.assertTrue("chroma was " + cam.getChroma(), Math.floor(cam.getChroma()) <= 12.0); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java index 4a6bbbcf1d6b..346d1e60fcf9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java @@ -24,7 +24,6 @@ import static com.android.systemui.qrcodescanner.controller.QRCodeScannerControl import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +31,7 @@ import static org.mockito.Mockito.when; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Resources; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; @@ -41,7 +41,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.DeviceConfigProxyFake; @@ -90,8 +89,9 @@ public class QRCodeScannerControllerTest extends SysuiTestCase { when(mPackageManager.queryIntentActivities(any(Intent.class), any(Integer.class))).thenReturn(resolveInfoList); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(true); - mContext.getOrCreateTestableResources().addOverride(R.string.def_qr_code_component, - defaultActivity); + mContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.string.config_defaultQrCodeComponent, defaultActivity); + mContext.getOrCreateTestableResources().addOverride( android.R.bool.config_enableQrCodeScannerOnLockScreen, enableOnLockScreen); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java new file mode 100644 index 000000000000..2927669020c8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -0,0 +1,285 @@ +/* + * 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.systemui.qs; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.IActivityManager; +import android.app.IForegroundServiceObserver; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.os.Binder; +import android.os.RemoteException; +import android.provider.DeviceConfig; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.DeviceConfigProxyFake; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class FgsManagerControllerTest extends SysuiTestCase { + + FakeSystemClock mSystemClock; + FakeExecutor mMainExecutor; + FakeExecutor mBackgroundExecutor; + DeviceConfigProxyFake mDeviceConfigProxyFake; + + @Mock + IActivityManager mIActivityManager; + @Mock + PackageManager mPackageManager; + @Mock + UserTracker mUserTracker; + @Mock + DialogLaunchAnimator mDialogLaunchAnimator; + @Mock + BroadcastDispatcher mBroadcastDispatcher; + @Mock + DumpManager mDumpManager; + + private FgsManagerController mFmc; + + private IForegroundServiceObserver mIForegroundServiceObserver; + private UserTracker.Callback mUserTrackerCallback; + private BroadcastReceiver mShowFgsManagerReceiver; + + private List<UserInfo> mUserProfiles; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + + mDeviceConfigProxyFake = new DeviceConfigProxyFake(); + mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED, "true", false); + mSystemClock = new FakeSystemClock(); + mMainExecutor = new FakeExecutor(mSystemClock); + mBackgroundExecutor = new FakeExecutor(mSystemClock); + + mUserProfiles = new ArrayList<>(); + Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles(); + + mFmc = createFgsManagerController(); + } + + @Test + public void testNumPackages() throws RemoteException { + setUserProfiles(0); + + Binder b1 = new Binder(); + Binder b2 = new Binder(); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, true); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, false); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + } + + @Test + public void testNumPackagesDoesNotChangeWhenSecondFgsIsStarted() throws RemoteException { + setUserProfiles(0); + + // Different tokens == different services + Binder b1 = new Binder(); + Binder b2 = new Binder(); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg1", 0, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg1", 0, false); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + } + + @Test + public void testNumPackagesListener() throws RemoteException { + setUserProfiles(0); + + FgsManagerController.OnNumberOfPackagesChangedListener onNumberOfPackagesChangedListener = + Mockito.mock(FgsManagerController.OnNumberOfPackagesChangedListener.class); + + mFmc.addOnNumberOfPackagesChangedListener(onNumberOfPackagesChangedListener); + + Binder b1 = new Binder(); + Binder b2 = new Binder(); + + verify(onNumberOfPackagesChangedListener, never()).onNumberOfPackagesChanged(anyInt()); + + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(1); + + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, true); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(2); + + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener, times(2)).onNumberOfPackagesChanged(1); + + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, false); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(0); + } + + @Test + public void testChangesSinceLastDialog() throws RemoteException { + setUserProfiles(0); + + Assert.assertFalse(mFmc.getChangesSinceDialog()); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true); + Assert.assertTrue(mFmc.getChangesSinceDialog()); + } + + @Test + public void testProfilePackagesCounted() throws RemoteException { + setUserProfiles(0, 10); + + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + } + + @Test + public void testSecondaryUserPackagesAreNotCounted() throws RemoteException { + setUserProfiles(0); + + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + } + + @Test + public void testSecondaryUserPackagesAreCountedWhenUserSwitch() throws RemoteException { + setUserProfiles(0); + + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg3", 10, true); + + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + + setUserProfiles(10); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + } + + + + FgsManagerController createFgsManagerController() throws RemoteException { + ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor = + ArgumentCaptor.forClass(IForegroundServiceObserver.class); + ArgumentCaptor<UserTracker.Callback> userTrackerCallbackArgumentCaptor = + ArgumentCaptor.forClass(UserTracker.Callback.class); + ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + FgsManagerController result = new FgsManagerController( + mContext, + mMainExecutor, + mBackgroundExecutor, + mSystemClock, + mIActivityManager, + mPackageManager, + mUserTracker, + mDeviceConfigProxyFake, + mDialogLaunchAnimator, + mBroadcastDispatcher, + mDumpManager + ); + result.init(); + + verify(mIActivityManager).registerForegroundServiceObserver( + iForegroundServiceObserverArgumentCaptor.capture() + ); + verify(mUserTracker).addCallback( + userTrackerCallbackArgumentCaptor.capture(), + ArgumentMatchers.eq(mBackgroundExecutor) + ); + verify(mBroadcastDispatcher).registerReceiver( + showFgsManagerReceiverArgumentCaptor.capture(), + argThat(fltr -> fltr.matchAction(Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER)), + eq(mMainExecutor), + isNull(), + eq(Context.RECEIVER_NOT_EXPORTED), + isNull() + ); + + mIForegroundServiceObserver = iForegroundServiceObserverArgumentCaptor.getValue(); + mUserTrackerCallback = userTrackerCallbackArgumentCaptor.getValue(); + mShowFgsManagerReceiver = showFgsManagerReceiverArgumentCaptor.getValue(); + + return result; + } + + private void setUserProfiles(int current, int... profileUserIds) { + mUserProfiles.clear(); + mUserProfiles.add(new UserInfo(current, "current:" + current, 0)); + for (int id : profileUserIds) { + mUserProfiles.add(new UserInfo(id, "profile:" + id, 0)); + } + + if (mUserTrackerCallback != null) { + mUserTrackerCallback.onUserChanged(current, mock(Context.class)); + mUserTrackerCallback.onProfilesChanged(mUserProfiles); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java new file mode 100644 index 000000000000..b86713d0890b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java @@ -0,0 +1,125 @@ +/* + * 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.systemui.qs.tiles; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.os.Handler; +import android.service.quicksettings.Tile; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.logging.MetricsLogger; +import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.classifier.FalsingManagerFake; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.qs.QSTileHost; +import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.statusbar.policy.DataSaverController; +import com.android.systemui.statusbar.policy.HotspotController; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@SmallTest +public class HotspotTileTest extends SysuiTestCase { + + @Rule + public MockitoRule mRule = MockitoJUnit.rule(); + @Mock + private QSTileHost mHost; + @Mock + private HotspotController mHotspotController; + @Mock + private DataSaverController mDataSaverController; + + private TestableLooper mTestableLooper; + private HotspotTile mTile; + private QSTile.BooleanState mState = new QSTile.BooleanState(); + + @Before + public void setUp() throws Exception { + mTestableLooper = TestableLooper.get(this); + when(mHost.getContext()).thenReturn(mContext); + when(mHost.getUserContext()).thenReturn(mContext); + when(mDataSaverController.isDataSaverEnabled()).thenReturn(false); + + mTile = new HotspotTile( + mHost, + mTestableLooper.getLooper(), + new Handler(mTestableLooper.getLooper()), + new FalsingManagerFake(), + mock(MetricsLogger.class), + mock(StatusBarStateController.class), + mock(ActivityStarter.class), + mock(QSLogger.class), + mHotspotController, + mDataSaverController + ); + + mTile.initialize(); + mTestableLooper.processAllMessages(); + } + + @Test + public void handleUpdateState_wifiTetheringIsAllowed_stateIsNotUnavailable() { + MockitoSession mockitoSession = ExtendedMockito.mockitoSession() + .spyStatic(WifiEnterpriseRestrictionUtils.class) + .startMocking(); + when(WifiEnterpriseRestrictionUtils.isWifiTetheringAllowed(mContext)).thenReturn(true); + + mTile.handleUpdateState(mState, null); + + assertThat(mState.state).isNotEqualTo(Tile.STATE_UNAVAILABLE); + assertThat(String.valueOf(mState.secondaryLabel)) + .isNotEqualTo(mContext.getString(R.string.wifitrackerlib_admin_restricted_network)); + mockitoSession.finishMocking(); + } + + @Test + public void handleUpdateState_wifiTetheringIsDisallowed_stateIsUnavailable() { + MockitoSession mockitoSession = ExtendedMockito.mockitoSession() + .spyStatic(WifiEnterpriseRestrictionUtils.class) + .startMocking(); + when(WifiEnterpriseRestrictionUtils.isWifiTetheringAllowed(mContext)).thenReturn(false); + + mTile.handleUpdateState(mState, null); + + assertThat(mState.state).isEqualTo(Tile.STATE_UNAVAILABLE); + assertThat(String.valueOf(mState.secondaryLabel)) + .isEqualTo(mContext.getString(R.string.wifitrackerlib_admin_restricted_network)); + mockitoSession.finishMocking(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 562c97017862..73e574eddd17 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -130,8 +130,8 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { whenever(statusbarStateController.state).thenReturn(StatusBarState.KEYGUARD) whenever(nsslController.isInLockedDownShade).thenReturn(false) whenever(qS.isFullyCollapsed).thenReturn(true) - whenever(lockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn( - true) + whenever(lockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(anyInt())) + .thenReturn(false) whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(true) whenever(lockScreenUserManager.isLockscreenPublicMode(anyInt())).thenReturn(true) whenever(falsingCollector.shouldEnforceBouncer()).thenReturn(false) @@ -207,8 +207,8 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { @Test fun testTriggeringBouncerWhenPrivateNotificationsArentAllowed() { - whenever(lockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn( - false) + whenever(lockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(anyInt())) + .thenReturn(true) transitionController.goToLockedShade(null) verify(statusbarStateController, never()).setState(anyInt()) verify(statusbarStateController).setLeaveOpenOnKeyguardHide(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 7687d1204541..18937e784ed1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -53,11 +53,13 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dependency; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.statusbar.NotificationLockscreenUserManager.KeyguardNotificationSuppressor; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -106,6 +108,10 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { private BroadcastDispatcher mBroadcastDispatcher; @Mock private KeyguardStateController mKeyguardStateController; + @Mock + private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + private OverviewProxyService mOverviewProxyService; private UserInfo mCurrentUser; private UserInfo mSecondaryUser; @@ -119,7 +125,6 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager); int currentUserId = ActivityManager.getCurrentUser(); mSettings = new FakeSettings(); @@ -212,7 +217,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false); // THEN current user's notification is redacted - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif)); } @Test @@ -223,7 +228,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false); // THEN current user's notification isn't redacted - assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); + assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif)); } @Test @@ -234,7 +239,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false); // THEN work profile notification is redacted - assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif)); } @Test @@ -245,7 +250,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false); // THEN work profile notification isn't redacted - assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif)); } @Test @@ -260,11 +265,11 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false); // THEN the work profile notification doesn't need to be redacted - assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif)); // THEN the current user and secondary user notifications do need to be redacted - assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); - assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif)); + assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mSecondaryUserNotif)); } @Test @@ -279,11 +284,11 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false); // THEN the work profile notification needs to be redacted - assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif)); + assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif)); // THEN the current user and secondary user notifications don't need to be redacted - assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif)); - assertFalse(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif)); + assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mSecondaryUserNotif)); } @Test @@ -298,7 +303,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // THEN the secondary profile notification still needs to be redacted because the current // user's setting takes precedence - assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif)); + assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mSecondaryUserNotif)); } @Test @@ -418,9 +423,12 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { context, mBroadcastDispatcher, mDevicePolicyManager, + mKeyguardUpdateMonitor, + () -> mEntryManager, + () -> mOverviewProxyService, mUserManager, - (() -> mVisibilityProvider), - (() -> mNotifCollection), + () -> mVisibilityProvider, + () -> mNotifCollection, mClickNotifier, NotificationLockscreenUserManagerTest.this.mKeyguardManager, mStatusBarStateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java index 7d06abf5cd67..e43ae0d2b22a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java @@ -124,8 +124,8 @@ public class DynamicPrivacyControllerTest extends SysuiTestCase { } private void allowPrivateNotificationsInPublic(boolean allow) { - when(mLockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn( - allow); + when(mLockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(anyInt())).thenReturn( + !allow); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java index 0fff5f5b47e5..16b0376ba1f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java @@ -294,7 +294,8 @@ public class NotificationEntryManagerTest extends SysuiTestCase { verify(mPresenter).updateNotificationViews(any()); verify(mEntryListener).onEntryRemoved( - eq(mEntry), any(), eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON)); + argThat(matchEntryOnKey()), any(), + eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON)); verify(mRow).setRemoved(); assertNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())); @@ -319,8 +320,8 @@ public class NotificationEntryManagerTest extends SysuiTestCase { mEntryManager.removeNotification("not_a_real_key", mRankingMap, UNDEFINED_DISMISS_REASON); - verify(mEntryListener, never()).onEntryRemoved( - eq(mEntry), any(), eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON)); + verify(mEntryListener, never()).onEntryRemoved(argThat(matchEntryOnKey()), any(), + eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON)); } /** Regression test for b/201097913. */ @@ -333,10 +334,10 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // Verify that only the listener for the NEW pipeline is notified. // Old pipeline: verify(mEntryListener, never()).onEntryRemoved( - argThat(matchEntryOnSbn()), any(), anyBoolean(), anyInt()); + argThat(matchEntryOnKey()), any(), anyBoolean(), anyInt()); // New pipeline: verify(mNotifCollectionListener).onEntryRemoved( - argThat(matchEntryOnSbn()), anyInt()); + argThat(matchEntryOnKey()), anyInt()); } @Test @@ -457,7 +458,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // THEN the notification is retained assertNotNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())); verify(mEntryListener, never()).onEntryRemoved( - eq(mEntry), any(), eq(false), eq(UNDEFINED_DISMISS_REASON)); + argThat(matchEntryOnKey()), any(), eq(false), eq(UNDEFINED_DISMISS_REASON)); } @Test @@ -476,7 +477,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // THEN the notification is removed assertNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())); verify(mEntryListener).onEntryRemoved( - eq(mEntry), any(), eq(false), eq(UNDEFINED_DISMISS_REASON)); + argThat(matchEntryOnKey()), any(), eq(false), eq(UNDEFINED_DISMISS_REASON)); } @Test @@ -541,7 +542,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // GIVEN interceptor that intercepts that entry when(mRemoveInterceptor.onNotificationRemoveRequested( - eq(mEntry.getKey()), eq(mEntry), anyInt())) + eq(mEntry.getKey()), argThat(matchEntryOnKey()), anyInt())) .thenReturn(true); // WHEN the notification is removed @@ -549,7 +550,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // THEN the interceptor intercepts & the entry is not removed & no listeners are called assertNotNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())); - verify(mEntryListener, never()).onEntryRemoved(eq(mEntry), + verify(mEntryListener, never()).onEntryRemoved(argThat(matchEntryOnKey()), any(NotificationVisibility.class), anyBoolean(), eq(UNDEFINED_DISMISS_REASON)); } @@ -560,7 +561,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // GIVEN interceptor that doesn't intercept when(mRemoveInterceptor.onNotificationRemoveRequested( - eq(mEntry.getKey()), eq(mEntry), anyInt())) + eq(mEntry.getKey()), argThat(matchEntryOnKey()), anyInt())) .thenReturn(false); // WHEN the notification is removed @@ -568,7 +569,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { // THEN the interceptor intercepts & the entry is not removed & no listeners are called assertNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())); - verify(mEntryListener, atLeastOnce()).onEntryRemoved(eq(mEntry), + verify(mEntryListener, atLeastOnce()).onEntryRemoved(argThat(matchEntryOnKey()), any(NotificationVisibility.class), anyBoolean(), eq(UNDEFINED_DISMISS_REASON)); } @@ -663,9 +664,8 @@ public class NotificationEntryManagerTest extends SysuiTestCase { PendingIntent.FLAG_IMMUTABLE)).build(); } - // TODO(b/201321631): Update more tests to use this function instead of eq(mEntry). - private ArgumentMatcher<NotificationEntry> matchEntryOnSbn() { - return e -> e.getSbn().equals(mSbn); + private ArgumentMatcher<NotificationEntry> matchEntryOnKey() { + return e -> e.getKey().equals(mEntry.getKey()); } private static class FakeNotificationLifetimeExtender implements NotificationLifetimeExtender { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java index 706800940fd1..958d54230f1c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java @@ -32,6 +32,8 @@ import static com.android.systemui.statusbar.notification.collection.Notificatio import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED; import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -47,6 +49,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static java.util.Collections.singletonList; @@ -180,13 +183,14 @@ public class NotifCollectionTest extends SysuiTestCase { @Test public void testGetGroupSummary() { - assertEquals(null, mCollection.getGroupSummary("group")); - NotifEvent summary = mNoMan.postNotif( - buildNotif(TEST_PACKAGE, 0) - .setGroup(mContext, "group") - .setGroupSummary(mContext, true)); + final NotificationEntryBuilder entryBuilder = buildNotif(TEST_PACKAGE, 0) + .setGroup(mContext, "group") + .setGroupSummary(mContext, true); + final String groupKey = entryBuilder.build().getSbn().getGroupKey(); + assertEquals(null, mCollection.getGroupSummary(groupKey)); + NotifEvent summary = mNoMan.postNotif(entryBuilder); - final NotificationEntry entry = mCollection.getGroupSummary("group"); + final NotificationEntry entry = mCollection.getGroupSummary(groupKey); assertEquals(summary.key, entry.getKey()); assertEquals(summary.sbn, entry.getSbn()); assertEquals(summary.ranking, entry.getRanking()); @@ -194,9 +198,9 @@ public class NotifCollectionTest extends SysuiTestCase { @Test public void testIsOnlyChildInGroup() { - NotifEvent notif1 = mNoMan.postNotif( - buildNotif(TEST_PACKAGE, 1) - .setGroup(mContext, "group")); + final NotificationEntryBuilder entryBuilder = buildNotif(TEST_PACKAGE, 1) + .setGroup(mContext, "group"); + NotifEvent notif1 = mNoMan.postNotif(entryBuilder); final NotificationEntry entry = mCollection.getEntry(notif1.key); assertTrue(mCollection.isOnlyChildInGroup(entry)); @@ -1488,6 +1492,55 @@ public class NotifCollectionTest extends SysuiTestCase { } @Test + public void testRegisterFutureDismissal() throws RemoteException { + // GIVEN a pipeline with one notification + NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotificationEntry entry = requireNonNull(mCollection.getEntry(notifEvent.key)); + clearInvocations(mCollectionListener); + + // WHEN registering a future dismissal, nothing happens right away + final Runnable onDismiss = mCollection.registerFutureDismissal(entry, REASON_CLICK, + NotifCollectionTest::defaultStats); + verifyNoMoreInteractions(mCollectionListener); + + // WHEN finally dismissing + onDismiss.run(); + verify(mStatusBarService).onNotificationClear(any(), anyInt(), eq(notifEvent.key), + anyInt(), anyInt(), any()); + verifyNoMoreInteractions(mStatusBarService); + verifyNoMoreInteractions(mCollectionListener); + } + + @Test + public void testRegisterFutureDismissalWithRetractionAndRepost() { + // GIVEN a pipeline with one notification + NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + NotificationEntry entry = requireNonNull(mCollection.getEntry(notifEvent.key)); + clearInvocations(mCollectionListener); + + // WHEN registering a future dismissal, nothing happens right away + final Runnable onDismiss = mCollection.registerFutureDismissal(entry, REASON_CLICK, + NotifCollectionTest::defaultStats); + verifyNoMoreInteractions(mCollectionListener); + + // WHEN retracting the notification, and then reposting + mNoMan.retractNotif(notifEvent.sbn, REASON_CLICK); + mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); + clearInvocations(mCollectionListener); + + // KNOWING that the entry in the collection is different now + assertThat(mCollection.getEntry(notifEvent.key)).isNotSameInstanceAs(entry); + + // WHEN finally dismissing + onDismiss.run(); + + // VERIFY that nothing happens; the notification should not be removed + verifyNoMoreInteractions(mCollectionListener); + assertThat(mCollection.getEntry(notifEvent.key)).isNotNull(); + verifyNoMoreInteractions(mStatusBarService); + } + + @Test public void testCannotDismissOngoingNotificationChildren() { // GIVEN an ongoing notification final NotificationEntry container = new NotificationEntryBuilder() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java index 9ea1813377a0..4e7e79f2cb26 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java @@ -1528,6 +1528,34 @@ public class ShadeListBuilderTest extends SysuiTestCase { } @Test + public void testContiguousSections() { + mListBuilder.setSectioners(List.of( + new PackageSectioner("pkg", 1), + new PackageSectioner("pkg", 1), + new PackageSectioner("pkg", 3), + new PackageSectioner("pkg", 2) + )); + } + + @Test(expected = IllegalStateException.class) + public void testNonContiguousSections() { + mListBuilder.setSectioners(List.of( + new PackageSectioner("pkg", 1), + new PackageSectioner("pkg", 1), + new PackageSectioner("pkg", 3), + new PackageSectioner("pkg", 1) + )); + } + + @Test(expected = IllegalStateException.class) + public void testBucketZeroNotAllowed() { + mListBuilder.setSectioners(List.of( + new PackageSectioner("pkg", 0), + new PackageSectioner("pkg", 1) + )); + } + + @Test public void testStabilizeGroupsDelayedSummaryRendersAllNotifsTopLevel() { // GIVEN group children posted without a summary addGroupChild(0, PACKAGE_1, GROUP_1); @@ -2189,7 +2217,11 @@ public class ShadeListBuilderTest extends SysuiTestCase { } PackageSectioner(String pkg) { - super("PackageSection_" + pkg, 0); + this(pkg, 0); + } + + PackageSectioner(String pkg, int bucket) { + super("PackageSection_" + pkg, bucket); mPackages = List.of(pkg); mComparator = null; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt deleted file mode 100644 index c6c043aafb20..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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.systemui.statusbar.notification.collection.coordinator - -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender -import com.android.systemui.statusbar.phone.NotifActivityLaunchEvents -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.withArgCaptor -import dagger.BindsInstance -import dagger.Component -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever - -@SmallTest -class ActivityLaunchAnimCoordinatorTest : SysuiTestCase() { - - val activityLaunchEvents: NotifActivityLaunchEvents = mock() - val pipeline: NotifPipeline = mock() - - val coordinator: ActivityLaunchAnimCoordinator = - DaggerTestActivityStarterCoordinatorComponent - .factory() - .create(activityLaunchEvents) - .coordinator - - @Test - fun testNoLifetimeExtensionIfNoAssociatedActivityLaunch() { - coordinator.attach(pipeline) - val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> { - verify(pipeline).addNotificationLifetimeExtender(capture()) - } - val fakeEntry = mock<NotificationEntry>().also { - whenever(it.key).thenReturn("0") - } - assertFalse(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0)) - } - - @Test - fun testNoLifetimeExtensionIfAssociatedActivityLaunchAlreadyEnded() { - coordinator.attach(pipeline) - val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> { - verify(pipeline).addNotificationLifetimeExtender(capture()) - } - val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> { - verify(activityLaunchEvents).registerListener(capture()) - } - val fakeEntry = mock<NotificationEntry>().also { - whenever(it.key).thenReturn("0") - } - eventListener.onStartLaunchNotifActivity(fakeEntry) - eventListener.onFinishLaunchNotifActivity(fakeEntry) - assertFalse(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0)) - } - - @Test - fun testLifetimeExtensionWhileActivityLaunchInProgress() { - coordinator.attach(pipeline) - val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> { - verify(pipeline).addNotificationLifetimeExtender(capture()) - } - val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> { - verify(activityLaunchEvents).registerListener(capture()) - } - val onEndLifetimeExtensionCallback = - mock<NotifLifetimeExtender.OnEndLifetimeExtensionCallback>() - lifetimeExtender.setCallback(onEndLifetimeExtensionCallback) - - val fakeEntry = mock<NotificationEntry>().also { - whenever(it.key).thenReturn("0") - } - eventListener.onStartLaunchNotifActivity(fakeEntry) - assertTrue(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0)) - - eventListener.onFinishLaunchNotifActivity(fakeEntry) - verify(onEndLifetimeExtensionCallback).onEndLifetimeExtension(lifetimeExtender, fakeEntry) - } - - @Test - fun testCancelLifetimeExtensionDoesNotInvokeCallback() { - coordinator.attach(pipeline) - val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> { - verify(pipeline).addNotificationLifetimeExtender(capture()) - } - val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> { - verify(activityLaunchEvents).registerListener(capture()) - } - val onEndLifetimeExtensionCallback = - mock<NotifLifetimeExtender.OnEndLifetimeExtensionCallback>() - lifetimeExtender.setCallback(onEndLifetimeExtensionCallback) - - val fakeEntry = mock<NotificationEntry>().also { - whenever(it.key).thenReturn("0") - } - eventListener.onStartLaunchNotifActivity(fakeEntry) - assertTrue(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0)) - - lifetimeExtender.cancelLifetimeExtension(fakeEntry) - eventListener.onFinishLaunchNotifActivity(fakeEntry) - verify(onEndLifetimeExtensionCallback, never()) - .onEndLifetimeExtension(lifetimeExtender, fakeEntry) - } -} - -@CoordinatorScope -@Component(modules = [ActivityLaunchAnimCoordinatorModule::class]) -interface TestActivityStarterCoordinatorComponent { - val coordinator: ActivityLaunchAnimCoordinator - - @Component.Factory - interface Factory { - fun create( - @BindsInstance activityLaunchEvents: NotifActivityLaunchEvents - ): TestActivityStarterCoordinatorComponent - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index f4d8405a796e..5c2f5fa6bc1c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -23,7 +23,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -33,7 +32,6 @@ import android.app.Notification; import android.app.NotificationManager; import android.testing.AndroidTestingRunner; -import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -41,7 +39,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SbnBuilder; import com.android.systemui.statusbar.notification.SectionClassifier; -import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -49,7 +46,6 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.NodeController; -import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController; import org.junit.Before; import org.junit.Test; @@ -72,7 +68,6 @@ public class RankingCoordinatorTest extends SysuiTestCase { @Mock private NotifPipeline mNotifPipeline; @Mock private NodeController mAlertingHeaderController; @Mock private NodeController mSilentNodeController; - @Mock private SectionHeaderController mSilentHeaderController; @Captor private ArgumentCaptor<NotifFilter> mNotifFilterCaptor; @@ -94,7 +89,6 @@ public class RankingCoordinatorTest extends SysuiTestCase { mHighPriorityProvider, mSectionClassifier, mAlertingHeaderController, - mSilentHeaderController, mSilentNodeController); mEntry = spy(new NotificationEntryBuilder().build()); mEntry.setRanking(getRankingForUnfilteredNotif().build()); @@ -112,25 +106,6 @@ public class RankingCoordinatorTest extends SysuiTestCase { } @Test - public void testSilentHeaderClearableChildrenUpdate() { - ListEntry listEntry = new ListEntry(mEntry.getKey(), 0L) { - @Nullable - @Override - public NotificationEntry getRepresentativeEntry() { - return mEntry; - } - }; - setRankingAmbient(false); - setSbnClearable(true); - mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry)); - verify(mSilentHeaderController).setClearSectionEnabled(eq(true)); - - setSbnClearable(false); - mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry)); - verify(mSilentHeaderController).setClearSectionEnabled(eq(false)); - } - - @Test public void testUnfilteredState() { // GIVEN no suppressed visual effects + app not suspended mEntry.setRanking(getRankingForUnfilteredNotif().build()); @@ -225,46 +200,6 @@ public class RankingCoordinatorTest extends SysuiTestCase { assertInSection(mEntry, mSilentSectioner); } - @Test - public void testClearableSilentSection() { - when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); - setSbnClearable(true); - setRankingAmbient(false); - mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - verify(mSilentHeaderController).setClearSectionEnabled(eq(true)); - } - - @Test - public void testClearableMinimizedSection() { - when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); - setSbnClearable(true); - setRankingAmbient(true); - mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - verify(mSilentHeaderController).setClearSectionEnabled(eq(true)); - } - - @Test - public void testNotClearableSilentSection() { - setSbnClearable(false); - when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); - setRankingAmbient(false); - mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - mAlertingSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - verify(mSilentHeaderController, times(2)).setClearSectionEnabled(eq(false)); - } - - @Test - public void testNotClearableMinimizedSection() { - setSbnClearable(false); - when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false); - setRankingAmbient(true); - mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - mAlertingSectioner.onEntriesUpdated(Arrays.asList(mEntry)); - verify(mSilentHeaderController, times(2)).setClearSectionEnabled(eq(false)); - } - private void assertInSection(NotificationEntry entry, NotifSectioner section) { for (NotifSectioner current: mSections) { if (current == section) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt deleted file mode 100644 index a2d8e3ddba24..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (C) 2021 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.systemui.statusbar.notification.collection.coordinator - -import android.os.UserHandle -import android.service.notification.StatusBarNotification -import androidx.test.filters.SmallTest -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.SysuiTestCase -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.statusbar.NotificationLockscreenUserManager -import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.notification.DynamicPrivacyController -import com.android.systemui.statusbar.notification.collection.ListEntry -import com.android.systemui.statusbar.notification.collection.NotifPipeline -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope -import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator -import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable -import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.withArgCaptor -import dagger.BindsInstance -import dagger.Component -import org.junit.Test -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever - -@SmallTest -class SensitiveContentCoordinatorTest : SysuiTestCase() { - - val dynamicPrivacyController: DynamicPrivacyController = mock() - val lockscreenUserManager: NotificationLockscreenUserManager = mock() - val pipeline: NotifPipeline = mock() - val keyguardUpdateMonitor: KeyguardUpdateMonitor = mock() - val statusBarStateController: StatusBarStateController = mock() - val keyguardStateController: KeyguardStateController = mock() - - val coordinator: SensitiveContentCoordinator = - DaggerTestSensitiveContentCoordinatorComponent - .factory() - .create( - dynamicPrivacyController, - lockscreenUserManager, - keyguardUpdateMonitor, - statusBarStateController, - keyguardStateController) - .coordinator - - @Test - fun onDynamicPrivacyChanged_invokeInvalidationListener() { - coordinator.attach(pipeline) - val invalidator = withArgCaptor<Invalidator> { - verify(pipeline).addPreRenderInvalidator(capture()) - } - val dynamicPrivacyListener = withArgCaptor<DynamicPrivacyController.Listener> { - verify(dynamicPrivacyController).addListener(capture()) - } - - val invalidationListener = mock<Pluggable.PluggableListener<Invalidator>>() - invalidator.setInvalidationListener(invalidationListener) - - dynamicPrivacyListener.onDynamicPrivacyChanged() - - verify(invalidationListener).onPluggableInvalidated(invalidator) - } - - @Test - fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) - val entry = fakeNotification(1, false) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!).setSensitive(false, false) - } - - @Test - fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) - val entry = fakeNotification(1, true) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!).setSensitive(false, false) - } - - @Test - fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) - val entry = fakeNotification(1, false) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!).setSensitive(false, false) - } - - @Test - fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) - val entry = fakeNotification(1, false) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!).setSensitive(false, true) - } - - @Test - fun onBeforeRenderList_deviceLocked_notifNeedsRedaction() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) - val entry = fakeNotification(1, true) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!).setSensitive(true, true) - } - - @Test - fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) - val entry = fakeNotification(1, true) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!).setSensitive(false, true) - } - - @Test - fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) - whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) - - val entry = fakeNotification(2, true) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!).setSensitive(true, true) - } - - @Test - fun onBeforeRenderList_deviceDynamicallyUnlocked_deviceBiometricBypassingLockScreen() { - coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } - - whenever(lockscreenUserManager.currentUserId).thenReturn(1) - whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) - whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) - whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) - whenever(statusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD) - whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(any())) - .thenReturn(true) - - val entry = fakeNotification(2, true) - - onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) - - verify(entry.representativeEntry!!, never()).setSensitive(any(), any()) - } - - private fun fakeNotification(notifUserId: Int, needsRedaction: Boolean): ListEntry { - val mockUserHandle = mock<UserHandle>().apply { - whenever(identifier).thenReturn(notifUserId) - } - val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply { - whenever(user).thenReturn(mockUserHandle) - } - val mockEntry = mock<NotificationEntry>().apply { - whenever(sbn).thenReturn(mockSbn) - } - whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction) - whenever(mockEntry.rowExists()).thenReturn(true) - return object : ListEntry("key", 0) { - override fun getRepresentativeEntry(): NotificationEntry = mockEntry - } - } -} - -@CoordinatorScope -@Component(modules = [SensitiveContentCoordinatorModule::class]) -interface TestSensitiveContentCoordinatorComponent { - val coordinator: SensitiveContentCoordinator - - @Component.Factory - interface Factory { - fun create( - @BindsInstance dynamicPrivacyController: DynamicPrivacyController, - @BindsInstance lockscreenUserManager: NotificationLockscreenUserManager, - @BindsInstance keyguardUpdateMonitor: KeyguardUpdateMonitor, - @BindsInstance statusBarStateController: StatusBarStateController, - @BindsInstance keyguardStateController: KeyguardStateController - ): TestSensitiveContentCoordinatorComponent - } -}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index 70266e401f8a..cf2fc7c3a5fe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator +import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest @@ -43,6 +44,11 @@ import org.mockito.Mockito.`when` as whenever @RunWith(AndroidTestingRunner::class) @RunWithLooper class StackCoordinatorTest : SysuiTestCase() { + + companion object { + const val NOTIF_USER_ID = 0 + } + private lateinit var coordinator: StackCoordinator private lateinit var afterRenderListListener: OnAfterRenderListListener @@ -61,7 +67,10 @@ class StackCoordinatorTest : SysuiTestCase() { afterRenderListListener = withArgCaptor { verify(pipeline).addOnAfterRenderListListener(capture()) } - entry = NotificationEntryBuilder().setSection(section).build() + entry = NotificationEntryBuilder() + .setSection(section) + .setUser(UserHandle.of(NOTIF_USER_ID)) + .build() } @Test @@ -74,13 +83,31 @@ class StackCoordinatorTest : SysuiTestCase() { fun testSetNotificationStats_clearableAlerting() { whenever(section.bucket).thenReturn(BUCKET_ALERTING) afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(stackController).setNotifStats(NotifStats(1, false, true, false, false)) + verify(stackController) + .setNotifStats( + NotifStats( + 1, + false, + true, + false, + false, + setOf(NOTIF_USER_ID), + emptySet())) } @Test fun testSetNotificationStats_clearableSilent() { whenever(section.bucket).thenReturn(BUCKET_SILENT) afterRenderListListener.onAfterRenderList(listOf(entry), stackController) - verify(stackController).setNotifStats(NotifStats(1, false, false, false, true)) + verify(stackController) + .setNotifStats( + NotifStats( + 1, + false, + false, + false, + true, + emptySet(), + setOf(NOTIF_USER_ID))) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt index b63e66f1ebe3..c7f7ec213b0c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.icon; +package com.android.systemui.statusbar.notification.icon -import android.app.ActivityManager; -import android.app.Notification; +import android.app.ActivityManager +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.app.Person @@ -27,11 +27,12 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.os.SystemClock import android.os.UserHandle -import android.testing.AndroidTestingRunner; +import android.testing.AndroidTestingRunner import androidx.test.InstrumentationRegistry import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any +import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection @@ -40,7 +41,7 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith; +import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.anyInt @@ -48,15 +49,14 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) -class IconManagerTest: SysuiTestCase() { +class IconManagerTest : SysuiTestCase() { companion object { - private const val TEST_PACKAGE_NAME = "test"; - private const val TEST_UID = 0; + private const val TEST_PACKAGE_NAME = "test" + private const val TEST_UID = 0 } - private var id = 0 - private val context = InstrumentationRegistry.getTargetContext(); + private val context = InstrumentationRegistry.getTargetContext() @Mock private lateinit var shortcut: ShortcutInfo @Mock private lateinit var shortcutIc: Icon @Mock private lateinit var messageIc: Icon @@ -65,6 +65,7 @@ class IconManagerTest: SysuiTestCase() { @Mock private lateinit var drawable: Drawable @Mock private lateinit var row: ExpandableNotificationRow + @Mock private lateinit var notifLockscreenUserManager: NotificationLockscreenUserManager @Mock private lateinit var notifCollection: CommonNotifCollection @Mock private lateinit var launcherApps: LauncherApps @@ -83,13 +84,16 @@ class IconManagerTest: SysuiTestCase() { `when`(shortcut.icon).thenReturn(shortcutIc) `when`(launcherApps.getShortcutIcon(shortcut)).thenReturn(shortcutIc) + `when`(notifLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(TEST_UID)) + .thenReturn(true) - iconManager = IconManager(notifCollection, launcherApps, iconBuilder) + iconManager = + IconManager(notifCollection, launcherApps, iconBuilder, notifLockscreenUserManager) } @Test fun testCreateIcons_importantConversation_shortcutIcon() { - val entry = notificationEntry(true, true, true) + val entry = notificationEntry() entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) @@ -99,7 +103,7 @@ class IconManagerTest: SysuiTestCase() { @Test fun testCreateIcons_importantConversation_messageIcon() { - val entry = notificationEntry(false, true, true) + val entry = notificationEntry(hasShortcut = false) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) @@ -109,7 +113,7 @@ class IconManagerTest: SysuiTestCase() { @Test fun testCreateIcons_importantConversation_largeIcon() { - val entry = notificationEntry(false, false, true) + val entry = notificationEntry(hasShortcut = false, hasMessage = false) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) @@ -119,7 +123,7 @@ class IconManagerTest: SysuiTestCase() { @Test fun testCreateIcons_importantConversation_smallIcon() { - val entry = notificationEntry(false, false, false) + val entry = notificationEntry(hasShortcut = false, hasMessage = false, hasLargeIcon = false) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) @@ -129,7 +133,7 @@ class IconManagerTest: SysuiTestCase() { @Test fun testCreateIcons_notImportantConversation() { - val entry = notificationEntry(true, true, true) + val entry = notificationEntry() entry?.let { iconManager.createIcons(it) } @@ -138,8 +142,10 @@ class IconManagerTest: SysuiTestCase() { @Test fun testCreateIcons_sensitiveImportantConversation() { - val entry = notificationEntry(true, false, false) - entry?.setSensitive(true, true); + val entry = notificationEntry( + hasMessage = false, + hasLargeIcon = false, + hasSensitiveContent = true) entry?.channel?.isImportantConversation = true entry?.let { iconManager.createIcons(it) @@ -151,14 +157,17 @@ class IconManagerTest: SysuiTestCase() { @Test fun testUpdateIcons_sensitivityChange() { - val entry = notificationEntry(true, false, false) + val entry = notificationEntry( + hasMessage = false, + hasLargeIcon = false, + hasSensitiveContent = true) entry?.channel?.isImportantConversation = true - entry?.setSensitive(true, true); entry?.let { iconManager.createIcons(it) } assertEquals(entry?.icons?.aodIcon?.sourceIcon, smallIc) - entry?.setSensitive(false, false); + `when`(notifLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(TEST_UID)) + .thenReturn(false) entry?.let { iconManager.updateIcons(it) } @@ -166,14 +175,19 @@ class IconManagerTest: SysuiTestCase() { } private fun notificationEntry( - hasShortcut: Boolean, - hasMessage: Boolean, - hasLargeIcon: Boolean + hasShortcut: Boolean = true, + hasMessage: Boolean = true, + hasLargeIcon: Boolean = true, + hasSensitiveContent: Boolean = false ): NotificationEntry? { val n = Notification.Builder(mContext, "id") .setSmallIcon(smallIc) .setContentTitle("Title") .setContentText("Text") + .setVisibility( + if (hasSensitiveContent) + Notification.VISIBILITY_PRIVATE + else Notification.VISIBILITY_PUBLIC) if (hasMessage) { n.style = Notification.MessagingStyle("") @@ -203,7 +217,6 @@ class IconManagerTest: SysuiTestCase() { val entry = builder.build() entry.row = row - entry.setSensitive(false, true); return entry } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 0f2e9bccc215..f5a0e2d48859 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -91,7 +91,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { @Test public void testGroupSummaryNotShowingIconWhenPublic() { - mGroupRow.setSensitive(true, true); + mGroupRow.setSensitive(true); mGroupRow.setHideSensitiveForIntrinsicHeight(true); assertTrue(mGroupRow.isSummaryWithChildren()); assertFalse(mGroupRow.isShowingIcon()); @@ -99,7 +99,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { @Test public void testNotificationHeaderVisibleWhenAnimating() { - mGroupRow.setSensitive(true, true); + mGroupRow.setSensitive(true); mGroupRow.setHideSensitive(true, false, 0, 0); mGroupRow.setHideSensitive(false, true, 0, 0); assertEquals(View.VISIBLE, mGroupRow.getChildrenContainer().getVisibleWrapper() @@ -130,7 +130,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { public void testIconColorShouldBeUpdatedWhenSensitive() throws Exception { ExpandableNotificationRow row = spy(mNotificationTestHelper.createRow( FLAG_CONTENT_VIEW_ALL)); - row.setSensitive(true, true); + row.setSensitive(true); row.setHideSensitive(true, false, 0, 0); verify(row).updateShelfIconColor(); } @@ -214,7 +214,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { @Test public void testFeedback_noHeader() { // public notification is custom layout - no header - mGroupRow.setSensitive(true, true); + mGroupRow.setSensitive(true); mGroupRow.setOnFeedbackClickListener(null); mGroupRow.setFeedbackIcon(null); } @@ -322,11 +322,22 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { ExpandableNotificationRow row = mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification()); row.getEntry().getChannel().setImportanceLockedByCriticalDeviceFunction(true); + row.getEntry().getChannel().setBlockable(false); assertTrue(row.getIsNonblockable()); } @Test + public void testGetIsNonblockable_criticalDeviceFunction_butBlockable() throws Exception { + ExpandableNotificationRow row = + mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification()); + row.getEntry().getChannel().setImportanceLockedByCriticalDeviceFunction(true); + row.getEntry().getChannel().setBlockable(true); + + assertFalse(row.getIsNonblockable()); + } + + @Test public void testCanDismissNoClear() throws Exception { ExpandableNotificationRow row = mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification()); @@ -335,7 +346,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { .build(); row.performDismiss(false); verify(mNotificationTestHelper.mOnUserInteractionCallback) - .onDismiss(any(), anyInt(), any()); + .registerFutureDismissal(any(), anyInt()); } @Test @@ -347,6 +358,6 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { .build(); row.performDismiss(false); verify(mNotificationTestHelper.mOnUserInteractionCallback, never()) - .onDismiss(any(), anyInt(), any()); + .registerFutureDismissal(any(), anyInt()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java index 251ac7d250fe..a8557050dddb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java @@ -78,7 +78,6 @@ import com.android.systemui.statusbar.notification.collection.legacy.Notificatio import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.icon.IconBuilder; import com.android.systemui.statusbar.notification.icon.IconManager; -import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; @@ -132,7 +131,6 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase { @Mock private NotificationEntryListener mEntryListener; @Mock private NotificationRowBinderImpl.BindRowCallback mBindCallback; @Mock private HeadsUpManager mHeadsUpManager; - @Mock private NotificationInterruptStateProvider mNotificationInterruptionStateProvider; @Mock private NotificationLockscreenUserManager mLockscreenUserManager; @Mock private NotificationGutsManager mGutsManager; @Mock private NotificationRemoteInputManager mRemoteInputManager; @@ -254,6 +252,7 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase { .thenAnswer((Answer<ExpandableNotificationRowController>) invocation -> new ExpandableNotificationRowController( viewCaptor.getValue(), + mLockscreenUserManager, mock(ActivatableNotificationViewController.class), mock(RemoteInputViewSubcomponent.Factory.class), mock(MetricsLogger.class), @@ -300,7 +299,8 @@ public class NotificationEntryManagerInflationTest extends SysuiTestCase { new IconManager( mEntryManager, mock(LauncherApps.class), - new IconBuilder(mContext)), + new IconBuilder(mContext), + mLockscreenUserManager), mock(LowPriorityInflationHelper.class), mNotifPipelineFlags); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 1ecb09bc8514..7a8b329bec64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -23,8 +23,11 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.Nullable; import android.app.ActivityManager; @@ -52,6 +55,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.media.MediaFeatureFlag; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -120,6 +124,7 @@ public class NotificationTestHelper { private StatusBarStateController mStatusBarStateController; private final PeopleNotificationIdentifier mPeopleNotificationIdentifier; public final OnUserInteractionCallback mOnUserInteractionCallback; + public final Runnable mFutureDismissalRunnable; public NotificationTestHelper( Context context, @@ -152,7 +157,8 @@ public class NotificationTestHelper { mIconManager = new IconManager( mock(CommonNotifCollection.class), mock(LauncherApps.class), - new IconBuilder(mContext)); + new IconBuilder(mContext), + mock(NotificationLockscreenUserManager.class)); NotificationContentInflater contentBinder = new NotificationContentInflater( mock(NotifRemoteViewCache.class), @@ -180,6 +186,9 @@ public class NotificationTestHelper { mBindPipelineEntryListener = collectionListenerCaptor.getValue(); mPeopleNotificationIdentifier = mock(PeopleNotificationIdentifier.class); mOnUserInteractionCallback = mock(OnUserInteractionCallback.class); + mFutureDismissalRunnable = mock(Runnable.class); + when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt())) + .thenReturn(mFutureDismissalRunnable); } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index 4270d72770dc..3f190367f284 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -5,8 +5,9 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.NotificationShelf -import junit.framework.Assert.assertFalse -import junit.framework.Assert.assertTrue +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.notification.row.ExpandableView +import junit.framework.Assert.* import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -143,6 +144,113 @@ class NotificationShelfTest : SysuiTestCase() { assertFalse(isYBelowShelfInView) } + @Test + fun getAmountInShelf_lastViewBelowShelf_completelyInShelf() { + val shelfClipStart = 0f + val viewStart = 1f + + val expandableView = mock(ExpandableView::class.java) + whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java)) + whenever(expandableView.translationY).thenReturn(viewStart) + whenever(expandableView.actualHeight).thenReturn(20) + + whenever(expandableView.minHeight).thenReturn(20) + whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY + whenever(expandableView.isInShelf).thenReturn(true) + + whenever(ambientState.isOnKeyguard).thenReturn(true) + whenever(ambientState.isExpansionChanging).thenReturn(false) + whenever(ambientState.isShadeExpanded).thenReturn(true) + + val amountInShelf = shelf.getAmountInShelf(/* i= */ 0, + /* view= */ expandableView, + /* scrollingFast= */ false, + /* expandingAnimated= */ false, + /* isLastChild= */ true, + shelfClipStart) + assertEquals(1f, amountInShelf) + } + + @Test + fun getAmountInShelf_lastViewAlmostBelowShelf_completelyInShelf() { + val viewStart = 0f + val shelfClipStart = 0.001f + + val expandableView = mock(ExpandableView::class.java) + whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java)) + whenever(expandableView.translationY).thenReturn(viewStart) + whenever(expandableView.actualHeight).thenReturn(20) + + whenever(expandableView.minHeight).thenReturn(20) + whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY + whenever(expandableView.isInShelf).thenReturn(true) + + whenever(ambientState.isOnKeyguard).thenReturn(true) + whenever(ambientState.isExpansionChanging).thenReturn(false) + whenever(ambientState.isShadeExpanded).thenReturn(true) + + val amountInShelf = shelf.getAmountInShelf(/* i= */ 0, + /* view= */ expandableView, + /* scrollingFast= */ false, + /* expandingAnimated= */ false, + /* isLastChild= */ true, + shelfClipStart) + assertEquals(1f, amountInShelf) + } + + @Test + fun getAmountInShelf_lastViewHalfClippedByShelf_halfInShelf() { + val viewStart = 0f + val shelfClipStart = 10f + + val expandableView = mock(ExpandableView::class.java) + whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java)) + whenever(expandableView.translationY).thenReturn(viewStart) + whenever(expandableView.actualHeight).thenReturn(25) + + whenever(expandableView.minHeight).thenReturn(25) + whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY + whenever(expandableView.isInShelf).thenReturn(true) + + whenever(ambientState.isOnKeyguard).thenReturn(true) + whenever(ambientState.isExpansionChanging).thenReturn(false) + whenever(ambientState.isShadeExpanded).thenReturn(true) + + val amountInShelf = shelf.getAmountInShelf(/* i= */ 0, + /* view= */ expandableView, + /* scrollingFast= */ false, + /* expandingAnimated= */ false, + /* isLastChild= */ true, + shelfClipStart) + assertEquals(0.5f, amountInShelf) + } + + @Test + fun getAmountInShelf_lastViewAboveShelf_notInShelf() { + val viewStart = 0f + val shelfClipStart = 15f + + val expandableView = mock(ExpandableView::class.java) + whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java)) + whenever(expandableView.translationY).thenReturn(viewStart) + whenever(expandableView.actualHeight).thenReturn(10) + + whenever(expandableView.minHeight).thenReturn(10) + whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY + whenever(expandableView.isInShelf).thenReturn(false) + + whenever(ambientState.isExpansionChanging).thenReturn(false) + whenever(ambientState.isOnKeyguard).thenReturn(true) + + val amountInShelf = shelf.getAmountInShelf(/* i= */ 0, + /* view= */ expandableView, + /* scrollingFast= */ false, + /* expandingAnimated= */ false, + /* isLastChild= */ true, + shelfClipStart) + assertEquals(0f, amountInShelf) + } + private fun setFractionToShade(fraction: Float) { whenever(ambientState.fractionToShade).thenReturn(fraction) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index e8608fa76c06..63e0f53e093d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -31,6 +31,8 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; @@ -612,6 +614,12 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { assertTrue(mStackScroller.isInsideQsHeader(event2)); } + @Test + public void setFractionToShade_recomputesStackHeight() { + mStackScroller.setFractionToShade(1f); + verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat()); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java index ed22cd3c55fc..169c04c692e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java @@ -36,6 +36,7 @@ import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.HeadsUpStatusBarView; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; @@ -57,9 +58,12 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { private final NotificationStackScrollLayoutController mStackScrollerController = mock(NotificationStackScrollLayoutController.class); - private final NotificationPanelViewController mPanelView = + private final NotificationPanelViewController mPanelViewController = mock(NotificationPanelViewController.class); private final DarkIconDispatcher mDarkIconDispatcher = mock(DarkIconDispatcher.class); + private final NotificationLockscreenUserManager mLockscreenUserManager = + mock(NotificationLockscreenUserManager.class); + private HeadsUpAppearanceController mHeadsUpAppearanceController; private ExpandableNotificationRow mFirst; private HeadsUpStatusBarView mHeadsUpStatusBarView; @@ -93,12 +97,13 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mHeadsUpManager, mStatusbarStateController, mBypassController, + mLockscreenUserManager, mWakeUpCoordinator, mDarkIconDispatcher, mKeyguardStateController, mCommandQueue, mStackScrollerController, - mPanelView, + mPanelViewController, mHeadsUpStatusBarView, new Clock(mContext, null), Optional.of(mOperatorNameView)); @@ -175,12 +180,13 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { mHeadsUpManager, mStatusbarStateController, mBypassController, + mLockscreenUserManager, mWakeUpCoordinator, mDarkIconDispatcher, mKeyguardStateController, mCommandQueue, mStackScrollerController, - mPanelView, + mPanelViewController, mHeadsUpStatusBarView, new Clock(mContext, null), Optional.empty()); @@ -193,15 +199,15 @@ public class HeadsUpAppearanceControllerTest extends SysuiTestCase { public void testDestroy() { reset(mHeadsUpManager); reset(mDarkIconDispatcher); - reset(mPanelView); + reset(mPanelViewController); reset(mStackScrollerController); mHeadsUpAppearanceController.onViewDetached(); verify(mHeadsUpManager).removeListener(any()); verify(mDarkIconDispatcher).removeDarkReceiver((DarkIconDispatcher.DarkReceiver) any()); - verify(mPanelView).removeTrackingHeadsUpListener(any()); - verify(mPanelView).setHeadsUpAppearanceController(isNull()); + verify(mPanelViewController).removeTrackingHeadsUpListener(any()); + verify(mPanelViewController).setHeadsUpAppearanceController(isNull()); verify(mStackScrollerController).removeOnExpandedHeightChangedListener(any()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java index f43c2a183465..9c02216722e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java @@ -475,4 +475,19 @@ public class KeyguardBouncerTest extends SysuiTestCase { mBouncer.setExpansion(bouncerHideAmount); verify(callback, never()).onExpansionChanged(bouncerHideAmount); } + + @Test + public void testOnResumeCalledForFullscreenBouncerOnSecondShow() { + // GIVEN a security mode which requires fullscreen bouncer + when(mKeyguardSecurityModel.getSecurityMode(anyInt())) + .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin); + mBouncer.show(true); + + // WHEN a second call to show occurs, the bouncer will already by visible + reset(mKeyguardHostViewController); + mBouncer.show(true); + + // THEN ensure the ViewController is told to resume + verify(mKeyguardHostViewController).onResume(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index dce520c0973d..5f8dda359563 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -1490,6 +1490,13 @@ public class ScrimControllerTest extends SysuiTestCase { assertAlphaAfterExpansion(mNotificationsScrim, 0f, expansion); } + @Test + public void aodStateSetsFrontScrimToNotBlend() { + mScrimController.transitionTo(ScrimState.AOD); + Assert.assertFalse("Front scrim should not blend with main color", + mScrimInFront.shouldBlendWithMainColor()); + } + private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) { mScrimController.setRawPanelExpansionFraction(expansion); finishAnimationsImmediately(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index fa867e2796f7..ecea14c6a522 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -85,7 +85,6 @@ import com.android.systemui.wmshell.BubblesManager; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; @@ -93,6 +92,7 @@ import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; import java.util.ArrayList; +import java.util.List; import java.util.Optional; @SmallTest @@ -141,12 +141,13 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { @Mock private OnUserInteractionCallback mOnUserInteractionCallback; @Mock + private Runnable mFutureDismissalRunnable; + @Mock private StatusBarNotificationActivityStarter mNotificationActivityStarter; @Mock private ActivityLaunchAnimator mActivityLaunchAnimator; @Mock private InteractionJankMonitor mJankMonitor; - private StatusBarNotificationActivityStarter.LaunchEventsEmitter mLaunchEventsEmitter; private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); private NotificationTestHelper mNotificationTestHelper; private ExpandableNotificationRow mNotificationRow; @@ -187,8 +188,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { when(mEntryManager.getVisibleNotifications()).thenReturn(mActiveNotifications); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); when(mNotifPipelineFlags.isNewPipelineEnabled()).thenReturn(false); - when(mOnUserInteractionCallback.getGroupSummaryToDismiss(mNotificationRow.getEntry())) - .thenReturn(null); + when(mOnUserInteractionCallback.registerFutureDismissal(eq(mNotificationRow.getEntry()), + anyInt())).thenReturn(mFutureDismissalRunnable); when(mVisibilityProvider.obtain(anyString(), anyBoolean())) .thenAnswer(invocation -> NotificationVisibility.obtain( invocation.getArgument(0), 0, 1, false)); @@ -203,7 +204,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { NotificationListContainer.class), headsUpManager, mJankMonitor); - mLaunchEventsEmitter = new StatusBarNotificationActivityStarter.LaunchEventsEmitter(); mNotificationActivityStarter = new StatusBarNotificationActivityStarter( getContext(), @@ -239,8 +239,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mock(NotificationPresenter.class), mock(NotificationPanelViewController.class), mActivityLaunchAnimator, - notificationAnimationProvider, - mLaunchEventsEmitter + notificationAnimationProvider ); // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg @@ -264,16 +263,23 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { @Test public void testOnNotificationClicked_keyGuardShowing() throws PendingIntent.CanceledException, RemoteException { + // To get the order right, collect posted runnables and run them later + List<Runnable> runnables = new ArrayList<>(); + doAnswer(answerVoid(r -> runnables.add((Runnable) r))) + .when(mHandler).post(any(Runnable.class)); // Given - StatusBarNotification sbn = mNotificationRow.getEntry().getSbn(); - sbn.getNotification().contentIntent = mContentIntent; - sbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL; + NotificationEntry entry = mNotificationRow.getEntry(); + Notification notification = entry.getSbn().getNotification(); + notification.contentIntent = mContentIntent; + notification.flags |= Notification.FLAG_AUTO_CANCEL; when(mKeyguardStateController.isShowing()).thenReturn(true); when(mCentralSurfaces.isOccluded()).thenReturn(true); // When - mNotificationActivityStarter.onNotificationClicked(sbn, mNotificationRow); + mNotificationActivityStarter.onNotificationClicked(entry, mNotificationRow); + // Run the collected runnables in fifo order, the way post() really does. + while (!runnables.isEmpty()) runnables.remove(0).run(); // Then verify(mShadeController, atLeastOnce()).collapsePanel(); @@ -283,24 +289,27 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); - InOrder orderVerifier = Mockito.inOrder(mClickNotifier, mOnUserInteractionCallback); - orderVerifier.verify(mClickNotifier).onNotificationClick( - eq(sbn.getKey()), any(NotificationVisibility.class)); + InOrder orderVerifier = Mockito.inOrder(mClickNotifier, mOnUserInteractionCallback, + mFutureDismissalRunnable); // Notification calls dismiss callback to remove notification due to FLAG_AUTO_CANCEL - orderVerifier.verify(mOnUserInteractionCallback).onDismiss(mNotificationRow.getEntry(), - REASON_CLICK, null); + orderVerifier.verify(mOnUserInteractionCallback) + .registerFutureDismissal(eq(entry), eq(REASON_CLICK)); + orderVerifier.verify(mClickNotifier).onNotificationClick( + eq(entry.getKey()), any(NotificationVisibility.class)); + orderVerifier.verify(mFutureDismissalRunnable).run(); } @Test public void testOnNotificationClicked_bubble_noContentIntent_noKeyGuard() throws RemoteException { - StatusBarNotification sbn = mBubbleNotificationRow.getEntry().getSbn(); + NotificationEntry entry = mBubbleNotificationRow.getEntry(); + StatusBarNotification sbn = entry.getSbn(); // Given sbn.getNotification().contentIntent = null; // When - mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow); + mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow); // Then verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry())); @@ -311,20 +320,22 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); verify(mClickNotifier).onNotificationClick( - eq(sbn.getKey()), any(NotificationVisibility.class)); + eq(entry.getKey()), any(NotificationVisibility.class)); // The content intent should NOT be sent on click. verifyZeroInteractions(mContentIntent); // Notification should not be cancelled. - verify(mOnUserInteractionCallback, never()).onDismiss(eq(mNotificationRow.getEntry()), - anyInt(), eq(null)); + verify(mOnUserInteractionCallback, never()) + .registerFutureDismissal(eq(mNotificationRow.getEntry()), anyInt()); + verify(mFutureDismissalRunnable, never()).run(); } @Test public void testOnNotificationClicked_bubble_noContentIntent_keyGuardShowing() throws RemoteException { - StatusBarNotification sbn = mBubbleNotificationRow.getEntry().getSbn(); + NotificationEntry entry = mBubbleNotificationRow.getEntry(); + StatusBarNotification sbn = entry.getSbn(); // Given sbn.getNotification().contentIntent = null; @@ -332,7 +343,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { when(mCentralSurfaces.isOccluded()).thenReturn(true); // When - mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow); + mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow); // Then verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry())); @@ -342,7 +353,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); verify(mClickNotifier).onNotificationClick( - eq(sbn.getKey()), any(NotificationVisibility.class)); + eq(entry.getKey()), any(NotificationVisibility.class)); // The content intent should NOT be sent on click. verifyZeroInteractions(mContentIntent); @@ -354,7 +365,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { @Test public void testOnNotificationClicked_bubble_withContentIntent_keyGuardShowing() throws RemoteException { - StatusBarNotification sbn = mBubbleNotificationRow.getEntry().getSbn(); + NotificationEntry entry = mBubbleNotificationRow.getEntry(); + StatusBarNotification sbn = entry.getSbn(); // Given sbn.getNotification().contentIntent = mContentIntent; @@ -362,7 +374,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { when(mCentralSurfaces.isOccluded()).thenReturn(true); // When - mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow); + mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow); // Then verify(mBubblesManager).expandStackAndSelectBubble(mBubbleNotificationRow.getEntry()); @@ -372,7 +384,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { verify(mAssistManager).hideAssist(); verify(mClickNotifier).onNotificationClick( - eq(sbn.getKey()), any(NotificationVisibility.class)); + eq(entry.getKey()), any(NotificationVisibility.class)); // The content intent should NOT be sent on click. verify(mContentIntent).getIntent(); @@ -405,57 +417,4 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { // THEN display should try wake up for the full screen intent verify(mCentralSurfaces).wakeUpForFullScreenIntent(); } - - @Test - public void testNotifActivityStarterEventSourceStartEvent_onNotificationClicked() { - NotifActivityLaunchEvents.Listener listener = - mock(NotifActivityLaunchEvents.Listener.class); - mLaunchEventsEmitter.registerListener(listener); - mNotificationActivityStarter - .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow); - verify(listener).onStartLaunchNotifActivity(mNotificationRow.getEntry()); - } - - @Test - public void testNotifActivityStarterEventSourceFinishEvent_dismissKeyguardCancelled() { - NotifActivityLaunchEvents.Listener listener = - mock(NotifActivityLaunchEvents.Listener.class); - mLaunchEventsEmitter.registerListener(listener); - // set up dismissKeyguardThenExecute to synchronously invoke the cancel runnable arg - doAnswer(answerVoid( - (OnDismissAction dismissAction, Runnable cancel, Boolean afterKeyguardGone) -> - cancel.run())) - .when(mActivityStarter) - .dismissKeyguardThenExecute(any(OnDismissAction.class), any(), anyBoolean()); - mNotificationActivityStarter - .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow); - verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry()); - } - - @Test - public void testNotifActivityStarterEventSourceFinishEvent_postPanelCollapse() - throws Exception { - NotifActivityLaunchEvents.Listener listener = - mock(NotifActivityLaunchEvents.Listener.class); - mLaunchEventsEmitter.registerListener(listener); - mNotificationActivityStarter - .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow); - ArgumentCaptor<ActivityLaunchAnimator.Controller> controllerCaptor = - ArgumentCaptor.forClass(ActivityLaunchAnimator.Controller.class); - verify(mActivityLaunchAnimator).startPendingIntentWithAnimation( - controllerCaptor.capture(), anyBoolean(), any(), any()); - controllerCaptor.getValue().onIntentStarted(false); - verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry()); - } - - @Test - public void testNotifActivityStarterEventSourceFinishEvent_postPanelCollapse_noAnimate() { - NotifActivityLaunchEvents.Listener listener = - mock(NotifActivityLaunchEvents.Listener.class); - mLaunchEventsEmitter.registerListener(listener); - when(mCentralSurfaces.shouldAnimateLaunch(anyBoolean())).thenReturn(false); - mNotificationActivityStarter - .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow); - verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry()); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 3dfc94bcd5b6..68818f610fb7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -74,6 +74,8 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.Executor; @@ -345,7 +347,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Test public void onSettingChanged_honorThemeStyle() { when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true); - for (Style style : Style.values()) { + List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT, + Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT); + for (Style style : validStyles) { reset(mSecureSettings); String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\"," diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java index de2efc71b3a9..8e4f184f560e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java @@ -100,7 +100,6 @@ public class QuickAccessWalletControllerTest extends SysuiTestCase { mContext, MoreExecutors.directExecutor(), MoreExecutors.directExecutor(), - MoreExecutors.directExecutor(), mSecureSettings, mQuickAccessWalletClient, mClock); diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml index ac1f0226be52..674bc749bc11 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml +++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml @@ -18,11 +18,11 @@ --> <resources> <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_height">16dp</dimen> + <dimen name="navigation_bar_height">24dp</dimen> <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height --> - <dimen name="navigation_bar_height_landscape">16dp</dimen> + <dimen name="navigation_bar_height_landscape">24dp</dimen> <!-- Width of the navigation bar when it is placed vertically on the screen --> - <dimen name="navigation_bar_width">16dp</dimen> + <dimen name="navigation_bar_width">24dp</dimen> <!-- Height of the bottom navigation / system bar. --> <dimen name="navigation_bar_frame_height">48dp</dimen> <!-- The height of the bottom navigation gesture area. --> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml index ac1f0226be52..674bc749bc11 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml +++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml @@ -18,11 +18,11 @@ --> <resources> <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_height">16dp</dimen> + <dimen name="navigation_bar_height">24dp</dimen> <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height --> - <dimen name="navigation_bar_height_landscape">16dp</dimen> + <dimen name="navigation_bar_height_landscape">24dp</dimen> <!-- Width of the navigation bar when it is placed vertically on the screen --> - <dimen name="navigation_bar_width">16dp</dimen> + <dimen name="navigation_bar_width">24dp</dimen> <!-- Height of the bottom navigation / system bar. --> <dimen name="navigation_bar_frame_height">48dp</dimen> <!-- The height of the bottom navigation gesture area. --> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml index ac1f0226be52..674bc749bc11 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml +++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml @@ -18,11 +18,11 @@ --> <resources> <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_height">16dp</dimen> + <dimen name="navigation_bar_height">24dp</dimen> <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height --> - <dimen name="navigation_bar_height_landscape">16dp</dimen> + <dimen name="navigation_bar_height_landscape">24dp</dimen> <!-- Width of the navigation bar when it is placed vertically on the screen --> - <dimen name="navigation_bar_width">16dp</dimen> + <dimen name="navigation_bar_width">24dp</dimen> <!-- Height of the bottom navigation / system bar. --> <dimen name="navigation_bar_frame_height">48dp</dimen> <!-- The height of the bottom navigation gesture area. --> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml index ac1f0226be52..674bc749bc11 100644 --- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml +++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml @@ -18,11 +18,11 @@ --> <resources> <!-- Height of the bottom navigation / system bar. --> - <dimen name="navigation_bar_height">16dp</dimen> + <dimen name="navigation_bar_height">24dp</dimen> <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height --> - <dimen name="navigation_bar_height_landscape">16dp</dimen> + <dimen name="navigation_bar_height_landscape">24dp</dimen> <!-- Width of the navigation bar when it is placed vertically on the screen --> - <dimen name="navigation_bar_width">16dp</dimen> + <dimen name="navigation_bar_width">24dp</dimen> <!-- Height of the bottom navigation / system bar. --> <dimen name="navigation_bar_frame_height">48dp</dimen> <!-- The height of the bottom navigation gesture area. --> diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 6d3620fa2ee6..ca0a7806898c 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -1965,15 +1965,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } } - final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds(); - - if (!hasAtLeastOneDataset && fieldClassificationIds == null) { - if (sVerbose) { - Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields " - + "classification ids)"); - } - return; - } for (int i = 0; i < mViewStates.size(); i++) { final ViewState viewState = mViewStates.valueAt(i); @@ -2022,6 +2013,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } continue; } + // Check if value match a dataset. if (hasAtLeastOneDataset) { for (int j = 0; j < responseCount; j++) { @@ -2078,7 +2070,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } // else } // for j } - } // else } // else } diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java index bc5cb0250d56..99526b7ef0d1 100644 --- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java +++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java @@ -26,7 +26,7 @@ import com.android.internal.backup.ITransportStatusCallback; public class TransportStatusCallback extends ITransportStatusCallback.Stub { private static final String TAG = "TransportStatusCallback"; - private static final int TIMEOUT_MILLIS = 600 * 1000; // 10 minutes. + private static final int TIMEOUT_MILLIS = 300 * 1000; // 5 minutes. private static final int OPERATION_STATUS_DEFAULT = 0; private final int mOperationTimeout; diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 9b2554fdbb3c..8622ef0940d5 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -649,6 +649,12 @@ public class CompanionDeviceManagerService extends SystemService { private void registerDevicePresenceListenerActive(String packageName, String deviceAddress, boolean active) throws RemoteException { + if (DEBUG) { + Log.i(TAG, "registerDevicePresenceListenerActive()" + + " active=" + active + + " deviceAddress=" + deviceAddress); + } + getContext().enforceCallingOrSelfPermission( android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE, "[un]registerDevicePresenceListenerService"); @@ -664,6 +670,12 @@ public class CompanionDeviceManagerService extends SystemService { + " for user " + userId)); } + // If already at specified state, then no-op. + if (active == association.isNotifyOnDeviceNearby()) { + if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state."); + return; + } + // AssociationInfo class is immutable: create a new AssociationInfo object with updated // flag. association = AssociationInfo.builder(association) @@ -674,7 +686,17 @@ public class CompanionDeviceManagerService extends SystemService { // an application sets/unsets the mNotifyOnDeviceNearby flag. mAssociationStore.updateAssociation(association); - // TODO(b/218615198): correctly handle the case when the device is currently present. + // If device is already present, then trigger callback. + if (active && mDevicePresenceMonitor.isDevicePresent(association.getId())) { + if (DEBUG) Log.d(TAG, "Device is already present. Triggering callback."); + onDeviceAppearedInternal(association.getId()); + } + + // If last listener is unregistered, then unbind application. + if (!active && !shouldBindPackage(userId, packageName)) { + if (DEBUG) Log.d(TAG, "Last listener unregistered. Unbinding application."); + mCompanionAppController.unbindCompanionApplication(userId, packageName); + } } @Override diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 80182d26003d..dc7573e20438 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -37,6 +37,7 @@ import android.util.ArrayMap; import android.util.Slog; import android.view.Display; import android.view.InputDevice; +import android.view.WindowManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -83,22 +84,26 @@ class InputController { private final NativeWrapper mNativeWrapper; private final DisplayManagerInternal mDisplayManagerInternal; private final InputManagerInternal mInputManagerInternal; + private final WindowManager mWindowManager; private final DeviceCreationThreadVerifier mThreadVerifier; - InputController(@NonNull Object lock, @NonNull Handler handler) { - this(lock, new NativeWrapper(), handler, + InputController(@NonNull Object lock, @NonNull Handler handler, + @NonNull WindowManager windowManager) { + this(lock, new NativeWrapper(), handler, windowManager, // Verify that virtual devices are not created on the handler thread. () -> !handler.getLooper().isCurrentThread()); } @VisibleForTesting InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper, - @NonNull Handler handler, @NonNull DeviceCreationThreadVerifier threadVerifier) { + @NonNull Handler handler, @NonNull WindowManager windowManager, + @NonNull DeviceCreationThreadVerifier threadVerifier) { mLock = lock; mHandler = handler; mNativeWrapper = nativeWrapper; mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); + mWindowManager = windowManager; mThreadVerifier = threadVerifier; } @@ -209,6 +214,15 @@ class InputController { mInputManagerInternal.setDisplayEligibilityForPointerCapture(displayId, isEligible); } + void setLocalIme(int displayId) { + // WM throws a SecurityException if the display is untrusted. + if ((mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED) + == Display.FLAG_TRUSTED) { + mWindowManager.setDisplayImePolicy(displayId, + WindowManager.DISPLAY_IME_POLICY_LOCAL); + } + } + @GuardedBy("mLock") private void updateActivePointerDisplayIdLocked() { InputDeviceDescriptor mostRecentlyCreatedMouse = null; diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 9802b9783da2..638b3aeef544 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -62,6 +62,7 @@ import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import android.view.Display; +import android.view.WindowManager; import android.widget.Toast; import android.window.DisplayWindowPolicyController; @@ -167,7 +168,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mParams = params; if (inputController == null) { mInputController = new InputController( - mVirtualDeviceLock, context.getMainThreadHandler()); + mVirtualDeviceLock, + context.getMainThreadHandler(), + context.getSystemService(WindowManager.class)); } else { mInputController = inputController; } @@ -537,6 +540,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mInputController.setPointerAcceleration(1f, displayId); mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false, displayId); + mInputController.setLocalIme(displayId); // Since we're being called in the middle of the display being created, we post a // task to grab the wakelock instead of doing it synchronously here, to avoid diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 61d784ecb5f3..ad6e7dbebae9 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -730,7 +730,7 @@ public final class ContentCaptureManagerService extends String serviceName = mServiceNameResolver.getServiceName(userId); ContentCaptureMetricsLogger.writeServiceEvent( EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST, - serviceName, request.getPackageName()); + serviceName); clientAdapter.error( ContentCaptureManager.DATA_SHARE_ERROR_CONCURRENT_REQUEST); } catch (RemoteException e) { @@ -1303,8 +1303,7 @@ public final class ContentCaptureManagerService extends private void logServiceEvent(int eventType) { int userId = UserHandle.getCallingUserId(); String serviceName = mParentService.mServiceNameResolver.getServiceName(userId); - ContentCaptureMetricsLogger.writeServiceEvent(eventType, serviceName, - mDataShareRequest.getPackageName()); + ContentCaptureMetricsLogger.writeServiceEvent(eventType, serviceName); } } } diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java index 7ea4eff3381c..10bec64936b3 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java @@ -34,72 +34,47 @@ public final class ContentCaptureMetricsLogger { } /** @hide */ - public static void writeServiceEvent(int eventType, @NonNull String serviceName, - @Nullable String targetPackage) { + public static void writeServiceEvent(int eventType, @NonNull String serviceName) { + // we should not logging the application package name FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS, eventType, - serviceName, targetPackage); - } - - /** @hide */ - public static void writeServiceEvent(int eventType, @NonNull ComponentName service, - @Nullable ComponentName target) { - writeServiceEvent(eventType, ComponentName.flattenToShortString(service), - ComponentName.flattenToShortString(target)); - } - - /** @hide */ - public static void writeServiceEvent(int eventType, @NonNull ComponentName service, - @Nullable String targetPackage) { - writeServiceEvent(eventType, ComponentName.flattenToShortString(service), targetPackage); + serviceName, /* componentName= */ null, 0, 0); } /** @hide */ public static void writeServiceEvent(int eventType, @NonNull ComponentName service) { - writeServiceEvent(eventType, ComponentName.flattenToShortString(service), null); + writeServiceEvent(eventType, ComponentName.flattenToShortString(service)); } /** @hide */ public static void writeSetWhitelistEvent(@Nullable ComponentName service, @Nullable List<String> packages, @Nullable List<ComponentName> activities) { final String serviceName = ComponentName.flattenToShortString(service); - StringBuilder stringBuilder = new StringBuilder(); - if (packages != null && packages.size() > 0) { - final int size = packages.size(); - stringBuilder.append(packages.get(0)); - for (int i = 1; i < size; i++) { - stringBuilder.append(" "); - stringBuilder.append(packages.get(i)); - } - } - if (activities != null && activities.size() > 0) { - stringBuilder.append(" "); - stringBuilder.append(activities.get(0).flattenToShortString()); - final int size = activities.size(); - for (int i = 1; i < size; i++) { - stringBuilder.append(" "); - stringBuilder.append(activities.get(i).flattenToShortString()); - } - } + int packageCount = packages != null ? packages.size() : 0; + int activityCount = activities != null ? activities.size() : 0; + // we should not logging the application package name + // log the allow list package and activity count instead FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS, FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__SET_WHITELIST, - serviceName, stringBuilder.toString()); + serviceName, /* allowListStr= */ null, packageCount, activityCount); } /** @hide */ public static void writeSessionEvent(int sessionId, int event, int flags, - @NonNull ComponentName service, @Nullable ComponentName app, boolean isChildSession) { + @NonNull ComponentName service, boolean isChildSession) { + // we should not logging the application package name FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS, sessionId, event, flags, ComponentName.flattenToShortString(service), - ComponentName.flattenToShortString(app), isChildSession); + /* componentName= */ null, isChildSession); } /** @hide */ public static void writeSessionFlush(int sessionId, @NonNull ComponentName service, - @Nullable ComponentName app, @NonNull FlushMetrics fm, - @NonNull ContentCaptureOptions options, int flushReason) { + @NonNull FlushMetrics fm, @NonNull ContentCaptureOptions options, + int flushReason) { + // we should not logging the application package name FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_FLUSHED, sessionId, ComponentName.flattenToShortString(service), - ComponentName.flattenToShortString(app), fm.sessionStarted, fm.sessionFinished, + /* componentName= */ null, fm.sessionStarted, fm.sessionFinished, fm.viewAppearedCount, fm.viewDisappearedCount, fm.viewTextChangedCount, options.maxBufferSize, options.idleFlushingFrequencyMs, options.textChangeFlushingFrequencyMs, flushReason); diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java index 822a42bf50cf..9bc1cee2aa05 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java @@ -330,7 +330,7 @@ final class ContentCapturePerUserService writeSessionEvent(sessionId, FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED, STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName, - componentName, /* isChildSession= */ false); + /* isChildSession= */ false); return; } if (serviceComponentName == null) { @@ -354,7 +354,7 @@ final class ContentCapturePerUserService writeSessionEvent(sessionId, FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED, STATE_DISABLED | STATE_NOT_WHITELISTED, serviceComponentName, - componentName, /* isChildSession= */ false); + /* isChildSession= */ false); return; } @@ -368,7 +368,7 @@ final class ContentCapturePerUserService writeSessionEvent(sessionId, FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED, STATE_DISABLED | STATE_DUPLICATED_ID, - serviceComponentName, componentName, /* isChildSession= */ false); + serviceComponentName, /* isChildSession= */ false); return; } @@ -385,7 +385,7 @@ final class ContentCapturePerUserService writeSessionEvent(sessionId, FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED, STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName, - componentName, /* isChildSession= */ false); + /* isChildSession= */ false); return; } @@ -740,7 +740,7 @@ final class ContentCapturePerUserService @Override public void writeSessionFlush(int sessionId, ComponentName app, FlushMetrics flushMetrics, ContentCaptureOptions options, int flushReason) { - ContentCaptureMetricsLogger.writeSessionFlush(sessionId, getServiceComponentName(), app, + ContentCaptureMetricsLogger.writeSessionFlush(sessionId, getServiceComponentName(), flushMetrics, options, flushReason); } diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java index 08e6a0550f69..1efe55aa0767 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java @@ -119,8 +119,7 @@ final class RemoteContentCaptureService // Metrics logging. writeSessionEvent(sessionId, FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__ON_SESSION_STARTED, - initialState, getComponentName(), context.getActivityComponent(), - /* is_child_session= */ false); + initialState, getComponentName(), /* is_child_session= */ false); } /** @@ -132,8 +131,7 @@ final class RemoteContentCaptureService // Metrics logging. writeSessionEvent(sessionId, FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__ON_SESSION_FINISHED, - /* flags= */ 0, getComponentName(), /* app= */ null, - /* is_child_session= */ false); + /* flags= */ 0, getComponentName(), /* is_child_session= */ false); } /** @@ -158,7 +156,7 @@ final class RemoteContentCaptureService scheduleAsyncRequest((s) -> s.onDataShared(request, dataShareCallback)); writeServiceEvent( FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_DATA_SHARE_REQUEST, - mComponentName, request.getPackageName()); + mComponentName); } /** diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index 06f698efde2b..ed6110086089 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -334,10 +334,14 @@ public abstract class PackageManagerInternal { /** * Retrieve all receivers that can handle a broadcast of the given intent. + * @param filterCallingUid The results will be filtered in the context of this UID instead + * of the calling UID. + * @param forSend true if the invocation is intended for sending broadcasts. The value + * of this parameter affects how packages are filtered. */ public abstract List<ResolveInfo> queryIntentReceivers(Intent intent, String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, - int filterCallingUid, int userId); + int filterCallingUid, int userId, boolean forSend); /** * Retrieve all services that can be performed for the given intent. @@ -371,10 +375,10 @@ public abstract class PackageManagerInternal { int deviceOwnerUserId, String deviceOwner, SparseArray<String> profileOwners); /** - * Called by Owners to set the package names protected by the device owner. + * Marks packages as protected for a given user or all users in case of USER_ALL. */ - public abstract void setDeviceOwnerProtectedPackages( - String deviceOwnerPackageName, List<String> packageNames); + public abstract void setOwnerProtectedPackages( + @UserIdInt int userId, @NonNull List<String> packageNames); /** * Returns {@code true} if a given package can't be wiped. Otherwise, returns {@code false}. diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index bc40170d39b7..5eec6e58e925 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -3880,9 +3880,12 @@ class StorageManagerService extends IStorageManager.Stub match = vol.isVisibleForWrite(userId) || (includeSharedProfile && vol.isVisibleForWrite(userIdSharingMedia)); } else { + // Return both read only and write only volumes. When includeSharedProfile is + // true, all the volumes of userIdSharingMedia should be returned when queried + // from the user it shares media with match = vol.isVisibleForUser(userId) || (!vol.isVisible() && includeInvisible && vol.getPath() != null) - || (includeSharedProfile && vol.isVisibleForRead(userIdSharingMedia)); + || (includeSharedProfile && vol.isVisibleForUser(userIdSharingMedia)); } if (!match) continue; diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 1033aea4b09f..7a52af61a196 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -3066,42 +3066,88 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { Binder.restoreCallingIdentity(ident); } + // Send the broadcast exactly once to all possible disjoint sets of apps. + // If the location master switch is on, broadcast the ServiceState 4 times: + // - Full ServiceState sent to apps with ACCESS_FINE_LOCATION and READ_PHONE_STATE + // - Full ServiceState sent to apps with ACCESS_FINE_LOCATION and + // READ_PRIVILEGED_PHONE_STATE but not READ_PHONE_STATE + // - Sanitized ServiceState sent to apps with READ_PHONE_STATE but not ACCESS_FINE_LOCATION + // - Sanitized ServiceState sent to apps with READ_PRIVILEGED_PHONE_STATE but neither + // READ_PHONE_STATE nor ACCESS_FINE_LOCATION + // If the location master switch is off, broadcast the ServiceState multiple times: + // - Full ServiceState sent to all apps permitted to bypass the location master switch if + // they have either READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE + // - Sanitized ServiceState sent to all other apps with READ_PHONE_STATE + // - Sanitized ServiceState sent to all other apps with READ_PRIVILEGED_PHONE_STATE but not + // READ_PHONE_STATE + if (Binder.withCleanCallingIdentity(() -> + LocationAccessPolicy.isLocationModeEnabled(mContext, mContext.getUserId()))) { + Intent fullIntent = createServiceStateIntent(state, subId, phoneId, false); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + fullIntent, + new String[]{Manifest.permission.READ_PHONE_STATE, + Manifest.permission.ACCESS_FINE_LOCATION}); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + fullIntent, + new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + Manifest.permission.ACCESS_FINE_LOCATION}, + new String[]{Manifest.permission.READ_PHONE_STATE}); + + Intent sanitizedIntent = createServiceStateIntent(state, subId, phoneId, true); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + sanitizedIntent, + new String[]{Manifest.permission.READ_PHONE_STATE}, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + sanitizedIntent, + new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE}, + new String[]{Manifest.permission.READ_PHONE_STATE, + Manifest.permission.ACCESS_FINE_LOCATION}); + } else { + String[] locationBypassPackages = Binder.withCleanCallingIdentity(() -> + LocationAccessPolicy.getLocationBypassPackages(mContext)); + for (String locationBypassPackage : locationBypassPackages) { + Intent fullIntent = createServiceStateIntent(state, subId, phoneId, false); + fullIntent.setPackage(locationBypassPackage); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + fullIntent, + new String[]{Manifest.permission.READ_PHONE_STATE}); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + fullIntent, + new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE}, + new String[]{Manifest.permission.READ_PHONE_STATE}); + } + + Intent sanitizedIntent = createServiceStateIntent(state, subId, phoneId, true); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + sanitizedIntent, + new String[]{Manifest.permission.READ_PHONE_STATE}, + new String[]{/* no excluded permissions */}, + locationBypassPackages); + mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions( + sanitizedIntent, + new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE}, + new String[]{Manifest.permission.READ_PHONE_STATE}, + locationBypassPackages); + } + } + + private Intent createServiceStateIntent(ServiceState state, int subId, int phoneId, + boolean sanitizeLocation) { Intent intent = new Intent(Intent.ACTION_SERVICE_STATE); intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); Bundle data = new Bundle(); - state.fillInNotifierBundle(data); + if (sanitizeLocation) { + state.createLocationInfoSanitizedCopy(true).fillInNotifierBundle(data); + } else { + state.fillInNotifierBundle(data); + } intent.putExtras(data); - // Pass the subscription along with the intent. intent.putExtra(PHONE_CONSTANTS_SUBSCRIPTION_KEY, subId); intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId); intent.putExtra(PHONE_CONSTANTS_SLOT_KEY, phoneId); intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, phoneId); - - // Send the broadcast twice -- once for all apps with READ_PHONE_STATE, then again - // for all apps with READ_PRIVILEGED_PHONE_STATE but not READ_PHONE_STATE. - // Do this again twice, the first time for apps with ACCESS_FINE_LOCATION, then again with - // the location-sanitized service state for all apps without ACCESS_FINE_LOCATION. - // This ensures that any app holding either READ_PRIVILEGED_PHONE_STATE or READ_PHONE_STATE - // get this broadcast exactly once, and we are not exposing location without permission. - mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent, - new String[] {Manifest.permission.READ_PHONE_STATE, - Manifest.permission.ACCESS_FINE_LOCATION}); - mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent, - new String[] {Manifest.permission.READ_PRIVILEGED_PHONE_STATE, - Manifest.permission.ACCESS_FINE_LOCATION}, - new String[] {Manifest.permission.READ_PHONE_STATE}); - - // Replace bundle with location-sanitized ServiceState - data = new Bundle(); - state.createLocationInfoSanitizedCopy(true).fillInNotifierBundle(data); - intent.putExtras(data); - mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent, - new String[] {Manifest.permission.READ_PHONE_STATE}, - new String[] {Manifest.permission.ACCESS_FINE_LOCATION}); - mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent, - new String[] {Manifest.permission.READ_PRIVILEGED_PHONE_STATE}, - new String[] {Manifest.permission.READ_PHONE_STATE, - Manifest.permission.ACCESS_FINE_LOCATION}); + return intent; } private void broadcastSignalStrengthChanged(SignalStrength signalStrength, int phoneId, diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java index c1d8e7bf3dc0..d3ef6bed46a0 100644 --- a/services/core/java/com/android/server/VpnManagerService.java +++ b/services/core/java/com/android/server/VpnManagerService.java @@ -880,6 +880,38 @@ public class VpnManagerService extends IVpnManager.Stub { } } + @Override + public boolean setAppExclusionList(int userId, String vpnPackage, List<String> excludedApps) { + enforceSettingsPermission(); + enforceCrossUserPermission(userId); + + synchronized (mVpns) { + final Vpn vpn = mVpns.get(userId); + if (vpn != null) { + return vpn.setAppExclusionList(vpnPackage, excludedApps); + } else { + logw("User " + userId + " has no Vpn configuration"); + throw new IllegalStateException( + "VPN for user " + userId + " not ready yet. Skipping setting the list"); + } + } + } + + @Override + public List<String> getAppExclusionList(int userId, String vpnPackage) { + enforceSettingsPermission(); + enforceCrossUserPermission(userId); + + synchronized (mVpns) { + final Vpn vpn = mVpns.get(userId); + if (vpn != null) { + return vpn.getAppExclusionList(vpnPackage); + } else { + logw("User " + userId + " has no Vpn configuration"); + return null; + } + } + } @Override public void factoryReset() { diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index fbf648278543..c678a67e5bd3 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -19,6 +19,8 @@ package com.android.server; import static com.android.server.Watchdog.HandlerCheckerAndTimeout.withCustomTimeout; import static com.android.server.Watchdog.HandlerCheckerAndTimeout.withDefaultTimeout; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.IActivityController; import android.content.BroadcastReceiver; import android.content.Context; @@ -43,6 +45,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; import android.sysprop.WatchdogProperties; +import android.util.Dumpable; import android.util.EventLog; import android.util.Log; import android.util.Slog; @@ -63,6 +66,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; @@ -76,7 +80,7 @@ import java.util.concurrent.TimeUnit; /** * This class calls its monitor every minute. Killing this process if they don't return **/ -public class Watchdog { +public class Watchdog implements Dumpable { static final String TAG = "Watchdog"; /** Debug flag. */ @@ -1028,4 +1032,10 @@ public class Watchdog { } doSysRq('c'); } + + @Override + public void dump(@NonNull PrintWriter pw, @Nullable String[] args) { + pw.print("WatchdogTimeoutMillis="); + pw.println(mWatchdogTimeoutMillis); + } } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 6fa3bc8e0669..48b3d0e106d3 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -6476,10 +6476,16 @@ public final class ActiveServices { } if (ret == REASON_DENIED) { - final boolean isAllowedPackage = - mAllowListWhileInUsePermissionInFgs.contains(callingPackage); - if (isAllowedPackage) { - ret = REASON_ALLOWLISTED_PACKAGE; + if (verifyPackage(callingPackage, callingUid)) { + final boolean isAllowedPackage = + mAllowListWhileInUsePermissionInFgs.contains(callingPackage); + if (isAllowedPackage) { + ret = REASON_ALLOWLISTED_PACKAGE; + } + } else { + EventLog.writeEvent(0x534e4554, "215003903", callingUid, + "callingPackage:" + callingPackage + " does not belong to callingUid:" + + callingUid); } } @@ -6593,12 +6599,11 @@ public final class ActiveServices { } final int uidState = mAm.getUidStateLocked(callingUid); - int callerTargetSdkVersion = INVALID_UID; + int callerTargetSdkVersion = -1; try { - ApplicationInfo ai = mAm.mContext.getPackageManager().getApplicationInfoAsUser( - callingPackage, PackageManager.MATCH_KNOWN_PACKAGES, userId); - callerTargetSdkVersion = ai.targetSdkVersion; - } catch (PackageManager.NameNotFoundException e) { + callerTargetSdkVersion = mAm.mContext.getPackageManager() + .getTargetSdkVersion(callingPackage); + } catch (PackageManager.NameNotFoundException ignored) { } final String debugInfo = "[callingPackage: " + callingPackage @@ -6883,4 +6888,19 @@ public final class ActiveServices { /* allowBackgroundActivityStarts */ false) != REASON_DENIED; } + + /** + * Checks if a given packageName belongs to a given uid. + * @param packageName the package of the caller + * @param uid the uid of the caller + * @return true or false + */ + private boolean verifyPackage(String packageName, int uid) { + if (uid == ROOT_UID || uid == SYSTEM_UID) { + //System and Root are always allowed + return true; + } + return mAm.getPackageManagerInternal().isSameApp(packageName, uid, + UserHandle.getUserId(uid)); + } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index dea8cc015272..f6e8bc826153 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2627,7 +2627,7 @@ public class ActivityManagerService extends IActivityManager.Stub public void batterySendBroadcast(Intent intent) { synchronized (this) { broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null, null, - OP_NONE, null, false, false, -1, SYSTEM_UID, Binder.getCallingUid(), + null, OP_NONE, null, false, false, -1, SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL); } } @@ -4241,7 +4241,7 @@ public class ActivityManagerService extends IActivityManager.Stub intent.putExtra(Intent.EXTRA_UID, uid); intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(uid)); broadcastIntentLocked(null, null, null, intent, - null, null, 0, null, null, null, null, OP_NONE, + null, null, 0, null, null, null, null, null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.getUserId(uid)); } @@ -8122,7 +8122,7 @@ public class ActivityManagerService extends IActivityManager.Stub | Intent.FLAG_RECEIVER_FOREGROUND); intent.putExtra(Intent.EXTRA_USER_HANDLE, currentUserId); broadcastIntentLocked(null, null, null, intent, - null, null, 0, null, null, null, null, OP_NONE, + null, null, 0, null, null, null, null, null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID, callingUid, callingPid, currentUserId); intent = new Intent(Intent.ACTION_USER_STARTING); @@ -8134,8 +8134,8 @@ public class ActivityManagerService extends IActivityManager.Stub public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) {} - }, 0, null, null, new String[] {INTERACT_ACROSS_USERS}, null, OP_NONE, - null, true, false, MY_PID, SYSTEM_UID, callingUid, callingPid, + }, 0, null, null, new String[] {INTERACT_ACROSS_USERS}, null, null, + OP_NONE, null, true, false, MY_PID, SYSTEM_UID, callingUid, callingPid, UserHandle.USER_ALL); } catch (Throwable e) { Slog.wtf(TAG, "Failed sending first user broadcasts", e); @@ -13165,8 +13165,8 @@ public class ActivityManagerService extends IActivityManager.Stub Intent intent = allSticky.get(i); BroadcastQueue queue = broadcastQueueForIntent(intent); BroadcastRecord r = new BroadcastRecord(queue, intent, null, - null, null, -1, -1, false, null, null, null, OP_NONE, null, receivers, - null, 0, null, null, false, true, true, -1, false, null, + null, null, -1, -1, false, null, null, null, null, OP_NONE, null, + receivers, null, 0, null, null, false, true, true, -1, false, null, false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */); queue.enqueueParallelBroadcastLocked(r); queue.scheduleBroadcastsLocked(); @@ -13248,8 +13248,8 @@ public class ActivityManagerService extends IActivityManager.Stub UserManager.DISALLOW_DEBUGGING_FEATURES, user)) { continue; } - List<ResolveInfo> newReceivers = mPackageManagerInt - .queryIntentReceivers(intent, resolvedType, pmFlags, callingUid, user); + List<ResolveInfo> newReceivers = mPackageManagerInt.queryIntentReceivers( + intent, resolvedType, pmFlags, callingUid, user, true /* forSend */); if (user != UserHandle.USER_SYSTEM && newReceivers != null) { // If this is not the system user, we need to check for // any receivers that should be filtered out. @@ -13265,8 +13265,9 @@ public class ActivityManagerService extends IActivityManager.Stub if (newReceivers != null) { for (int i = newReceivers.size() - 1; i >= 0; i--) { final ResolveInfo ri = newReceivers.get(i); - final Resolution<ResolveInfo> resolution = mComponentAliasResolver - .resolveReceiver(intent, ri, resolvedType, pmFlags, user, callingUid); + final Resolution<ResolveInfo> resolution = + mComponentAliasResolver.resolveReceiver(intent, ri, resolvedType, + pmFlags, user, callingUid, true /* forSend */); if (resolution == null) { // It was an alias, but the target was not found. newReceivers.remove(i); @@ -13421,12 +13422,14 @@ public class ActivityManagerService extends IActivityManager.Stub String callerPackage, String callerFeatureId, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras, String[] requiredPermissions, String[] excludedPermissions, - int appOp, Bundle bOptions, boolean ordered, boolean sticky, int callingPid, + String[] excludedPackages, int appOp, Bundle bOptions, boolean ordered, + boolean sticky, int callingPid, int callingUid, int realCallingUid, int realCallingPid, int userId) { return broadcastIntentLocked(callerApp, callerPackage, callerFeatureId, intent, resolvedType, resultTo, resultCode, resultData, resultExtras, requiredPermissions, - excludedPermissions, appOp, bOptions, ordered, sticky, callingPid, callingUid, - realCallingUid, realCallingPid, userId, false /* allowBackgroundActivityStarts */, + excludedPermissions, excludedPackages, appOp, bOptions, ordered, sticky, callingPid, + callingUid, realCallingUid, realCallingPid, userId, + false /* allowBackgroundActivityStarts */, null /* tokenNeededForBackgroundActivityStarts */, null /* broadcastAllowList */); } @@ -13435,7 +13438,7 @@ public class ActivityManagerService extends IActivityManager.Stub @Nullable String callerFeatureId, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras, String[] requiredPermissions, - String[] excludedPermissions, int appOp, Bundle bOptions, + String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions, boolean ordered, boolean sticky, int callingPid, int callingUid, int realCallingUid, int realCallingPid, int userId, boolean allowBackgroundActivityStarts, @@ -14042,10 +14045,10 @@ public class ActivityManagerService extends IActivityManager.Stub final BroadcastQueue queue = broadcastQueueForIntent(intent); BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType, - requiredPermissions, excludedPermissions, appOp, brOptions, registeredReceivers, - resultTo, resultCode, resultData, resultExtras, ordered, sticky, false, userId, - allowBackgroundActivityStarts, backgroundActivityStartsToken, - timeoutExempt); + requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions, + registeredReceivers, resultTo, resultCode, resultData, resultExtras, ordered, + sticky, false, userId, allowBackgroundActivityStarts, + backgroundActivityStartsToken, timeoutExempt); if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing parallel broadcast " + r); final boolean replaced = replacePending && (queue.replaceParallelBroadcastLocked(r) != null); @@ -14140,7 +14143,7 @@ public class ActivityManagerService extends IActivityManager.Stub BroadcastQueue queue = broadcastQueueForIntent(intent); BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType, - requiredPermissions, excludedPermissions, appOp, brOptions, + requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions, receivers, resultTo, resultCode, resultData, resultExtras, ordered, sticky, false, userId, allowBackgroundActivityStarts, backgroundActivityStartsToken, timeoutExempt); @@ -14269,14 +14272,16 @@ public class ActivityManagerService extends IActivityManager.Stub String[] requiredPermissions, int appOp, Bundle bOptions, boolean serialized, boolean sticky, int userId) { return broadcastIntentWithFeature(caller, null, intent, resolvedType, resultTo, resultCode, - resultData, resultExtras, requiredPermissions, null, appOp, bOptions, serialized, - sticky, userId); + resultData, resultExtras, requiredPermissions, null, null, appOp, bOptions, + serialized, sticky, userId); } + @Override public final int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras, - String[] requiredPermissions, String[] excludedPermissions, int appOp, Bundle bOptions, + String[] requiredPermissions, String[] excludedPermissions, + String[] excludedPackages, int appOp, Bundle bOptions, boolean serialized, boolean sticky, int userId) { enforceNotIsolatedCaller("broadcastIntent"); synchronized(this) { @@ -14291,8 +14296,8 @@ public class ActivityManagerService extends IActivityManager.Stub return broadcastIntentLocked(callerApp, callerApp != null ? callerApp.info.packageName : null, callingFeatureId, intent, resolvedType, resultTo, resultCode, resultData, resultExtras, - requiredPermissions, excludedPermissions, appOp, bOptions, serialized, - sticky, callingPid, callingUid, callingUid, callingPid, userId); + requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions, + serialized, sticky, callingPid, callingUid, callingUid, callingPid, userId); } finally { Binder.restoreCallingIdentity(origId); } @@ -14315,7 +14320,7 @@ public class ActivityManagerService extends IActivityManager.Stub try { return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType, resultTo, resultCode, resultData, resultExtras, requiredPermissions, null, - OP_NONE, bOptions, serialized, sticky, -1, uid, realCallingUid, + null, OP_NONE, bOptions, serialized, sticky, -1, uid, realCallingUid, realCallingPid, userId, allowBackgroundActivityStarts, backgroundActivityStartsToken, broadcastAllowList); } finally { @@ -16834,10 +16839,11 @@ public class ActivityManagerService extends IActivityManager.Stub return ActivityManagerService.this.broadcastIntentLocked(null /*callerApp*/, null /*callerPackage*/, null /*callingFeatureId*/, intent, null /*resolvedType*/, resultTo, 0 /*resultCode*/, null /*resultData*/, - null /*resultExtras*/, requiredPermissions, null, AppOpsManager.OP_NONE, - bOptions /*options*/, serialized, false /*sticky*/, callingPid, - callingUid, callingUid, callingPid, userId, - false /*allowBackgroundStarts*/, + null /*resultExtras*/, requiredPermissions, + null /*excludedPermissions*/, null /*excludedPackages*/, + AppOpsManager.OP_NONE, bOptions /*options*/, serialized, + false /*sticky*/, callingPid, callingUid, callingUid, callingPid, + userId, false /*allowBackgroundStarts*/, null /*tokenNeededForBackgroundActivityStarts*/, appIdAllowList); } finally { Binder.restoreCallingIdentity(origId); @@ -16973,7 +16979,7 @@ public class ActivityManagerService extends IActivityManager.Stub | Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null, - null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID, + null, null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL); if ((changes & ActivityInfo.CONFIG_LOCALE) != 0) { intent = new Intent(Intent.ACTION_LOCALE_CHANGED); @@ -16988,8 +16994,8 @@ public class ActivityManagerService extends IActivityManager.Stub TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED, PowerExemptionManager.REASON_LOCALE_CHANGED, ""); broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null, - null, OP_NONE, bOptions.toBundle(), false, false, MY_PID, SYSTEM_UID, - Binder.getCallingUid(), Binder.getCallingPid(), + null, null, OP_NONE, bOptions.toBundle(), false, false, MY_PID, + SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL); } @@ -17004,8 +17010,9 @@ public class ActivityManagerService extends IActivityManager.Stub String[] permissions = new String[] { android.Manifest.permission.INSTALL_PACKAGES }; broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, - permissions, null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID, - Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL); + permissions, null, null, OP_NONE, null, false, false, MY_PID, + SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), + UserHandle.USER_ALL); } } } @@ -17029,8 +17036,8 @@ public class ActivityManagerService extends IActivityManager.Stub } broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null, - null, OP_NONE, null, false, false, -1, SYSTEM_UID, Binder.getCallingUid(), - Binder.getCallingPid(), UserHandle.USER_ALL); + null, null, OP_NONE, null, false, false, -1, SYSTEM_UID, + Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL); } } @@ -17195,17 +17202,17 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public boolean isUidCurrentlyInstrumented(int uid) { + public int getInstrumentationSourceUid(int uid) { synchronized (mProcLock) { for (int i = mActiveInstrumentation.size() - 1; i >= 0; i--) { ActiveInstrumentation activeInst = mActiveInstrumentation.get(i); if (!activeInst.mFinished && activeInst.mTargetInfo != null && activeInst.mTargetInfo.uid == uid) { - return true; + return activeInst.mSourceUid; } } } - return false; + return INVALID_UID; } @Override diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 402491d8fe80..397a4420700e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -804,8 +804,8 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.flush(); Bundle bundle = mBroadcastOptions == null ? null : mBroadcastOptions.toBundle(); mInterface.broadcastIntentWithFeature(null, null, intent, null, receiver, 0, null, null, - requiredPermissions, null, android.app.AppOpsManager.OP_NONE, bundle, true, false, - mUserId); + requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, bundle, true, + false, mUserId); if (!mAsync) { receiver.waitForFinish(); } diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java index c09bb2d4dc28..1a566a9e2bc5 100644 --- a/services/core/java/com/android/server/am/AppBatteryTracker.java +++ b/services/core/java/com/android/server/am/AppBatteryTracker.java @@ -1258,6 +1258,16 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> + "current_drain_event_duration_based_threshold_enabled"; /** + * Whether or not we should move the app into the restricted bucket level if its background + * battery usage goes beyond the threshold. Note this different from the flag + * {@link AppRestrictionController.ConstantsObserver#KEY_BG_AUTO_RESTRICT_ABUSIVE_APPS} + * which is to control the overall auto bg restrictions. + */ + static final String KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED = + DEVICE_CONFIG_SUBNAMESPACE_PREFIX + + "current_drain_auto_restrict_abusive_apps_enabled"; + + /** * The types of battery drain we're checking on each app; if the sum of the battery drain * exceeds the threshold, it'll be moved to restricted standby bucket; the type here * must be one of, or combination of {@link #BATTERY_USAGE_TYPE_BACKGROUND}, @@ -1353,6 +1363,11 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> final boolean mDefaultBgCurrentDrainEventDurationBasedThresholdEnabled; /** + * Default value to {@link #mBgCurrentDrainAutoRestrictAbusiveAppsEnabled}. + */ + final boolean mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled; + + /** * Default value to {@link #mBgCurrentDrainRestrictedBucketTypes}. */ final int mDefaultCurrentDrainTypesToRestrictedBucket; @@ -1430,6 +1445,11 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> volatile boolean mBgCurrentDrainEventDurationBasedThresholdEnabled; /** + * @see #KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED. + */ + volatile boolean mBgCurrentDrainAutoRestrictAbusiveAppsEnabled; + + /** * @see #KEY_BG_CURRENT_DRAIN_TYPES_TO_RESTRICTED_BUCKET. */ volatile int mBgCurrentDrainRestrictedBucketTypes; @@ -1520,6 +1540,8 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> R.integer.config_bg_current_drain_location_min_duration) * 1_000; mDefaultBgCurrentDrainEventDurationBasedThresholdEnabled = resources.getBoolean( R.bool.config_bg_current_drain_event_duration_based_threshold_enabled); + mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled = resources.getBoolean( + R.bool.config_bg_current_drain_auto_restrict_abusive_apps); mDefaultCurrentDrainTypesToRestrictedBucket = resources.getInteger( R.integer.config_bg_current_drain_types_to_restricted_bucket); mDefaultBgCurrentDrainTypesToBgRestricted = resources.getInteger( @@ -1569,6 +1591,9 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> case KEY_BG_CURRENT_DRAIN_POWER_COMPONENTS: updateCurrentDrainThreshold(); break; + case KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED: + updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled(); + break; case KEY_BG_CURRENT_DRAIN_WINDOW: updateCurrentDrainWindow(); break; @@ -1701,6 +1726,13 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> DEFAULT_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLD); } + private void updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled() { + mBgCurrentDrainAutoRestrictAbusiveAppsEnabled = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED, + mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled); + } + @Override public void onSystemReady() { mBatteryFullChargeMah = @@ -1714,6 +1746,7 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> updateCurrentDrainEventDurationBasedThresholdEnabled(); updateCurrentDrainExemptedTypes(); updateCurrentDrainDecoupleThresholds(); + updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled(); } @Override @@ -1728,9 +1761,12 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> if (pair != null) { final long lastInteractionTime = mLastInteractionTime.get(uid, 0L); final long[] ts = pair.first; - final int restrictedLevel = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET] - > (lastInteractionTime + mBgCurrentDrainInteractionGracePeriodMs) - && mTracker.mAppRestrictionController.isAutoRestrictAbusiveAppEnabled() + final boolean noInteractionRecently = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET] + > (lastInteractionTime + mBgCurrentDrainInteractionGracePeriodMs); + final boolean canRestrict = + mTracker.mAppRestrictionController.isAutoRestrictAbusiveAppEnabled() + && mBgCurrentDrainAutoRestrictAbusiveAppsEnabled; + final int restrictedLevel = noInteractionRecently && canRestrict ? RESTRICTION_LEVEL_RESTRICTED_BUCKET : RESTRICTION_LEVEL_ADAPTIVE_BUCKET; if (maxLevel > RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) { @@ -2066,6 +2102,10 @@ final class AppBatteryTracker extends BaseAppStateTracker<AppBatteryPolicy> pw.print('='); pw.println(mBgCurrentDrainEventDurationBasedThresholdEnabled); pw.print(prefix); + pw.print(KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED); + pw.print('='); + pw.println(mBgCurrentDrainAutoRestrictAbusiveAppsEnabled); + pw.print(prefix); pw.print(KEY_BG_CURRENT_DRAIN_TYPES_TO_RESTRICTED_BUCKET); pw.print('='); pw.println(batteryUsageTypesToString(mBgCurrentDrainRestrictedBucketTypes)); diff --git a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java index 84969aa9e916..702526a4beab 100644 --- a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java +++ b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java @@ -320,12 +320,11 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync { } @Override - public Future<?> scheduleSyncDueToProcessStateChange(long delayMillis) { + public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) { synchronized (BatteryExternalStatsWorker.this) { mProcessStateSync = scheduleDelayedSyncLocked(mProcessStateSync, - () -> scheduleSync("procstate-change", UPDATE_ON_PROC_STATE_CHANGE), + () -> scheduleSync("procstate-change", flags), delayMillis); - return mProcessStateSync; } } diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java index dd7fb84b46bf..d2e40c56c772 100644 --- a/services/core/java/com/android/server/am/BroadcastQueue.java +++ b/services/core/java/com/android/server/am/BroadcastQueue.java @@ -860,6 +860,21 @@ public final class BroadcastQueue { } } + // Check that the receiver does *not* belong to any of the excluded packages + if (!skip && r.excludedPackages != null && r.excludedPackages.length > 0) { + if (ArrayUtils.contains(r.excludedPackages, filter.packageName)) { + Slog.w(TAG, "Skipping delivery of excluded package " + + r.intent.toString() + + " to " + filter.receiverList.app + + " (pid=" + filter.receiverList.pid + + ", uid=" + filter.receiverList.uid + ")" + + " excludes package " + filter.packageName + + " due to sender " + r.callerPackage + + " (uid " + r.callingUid + ")"); + skip = true; + } + } + // If the broadcast also requires an app op check that as well. if (!skip && r.appOp != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(r.appOp, @@ -1721,6 +1736,19 @@ public final class BroadcastQueue { } } + // Check that the receiver does *not* belong to any of the excluded packages + if (!skip && r.excludedPackages != null && r.excludedPackages.length > 0) { + if (ArrayUtils.contains(r.excludedPackages, component.getPackageName())) { + Slog.w(TAG, "Skipping delivery of excluded package " + + r.intent + " to " + + component.flattenToShortString() + + " excludes package " + component.getPackageName() + + " due to sender " + r.callerPackage + + " (uid " + r.callingUid + ")"); + skip = true; + } + } + if (!skip && info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && r.requiredPermissions != null && r.requiredPermissions.length > 0) { for (int i = 0; i < r.requiredPermissions.length; i++) { diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 5343af25fd39..19ffc1733f3d 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -75,6 +75,7 @@ final class BroadcastRecord extends Binder { final String resolvedType; // the resolved data type final String[] requiredPermissions; // permissions the caller has required final String[] excludedPermissions; // permissions to exclude + final String[] excludedPackages; // packages to exclude final int appOp; // an app op that is associated with this broadcast final BroadcastOptions options; // BroadcastOptions supplied by caller final List receivers; // contains BroadcastFilter and ResolveInfo @@ -162,6 +163,10 @@ final class BroadcastRecord extends Binder { pw.print(prefix); pw.print("excludedPermissions="); pw.print(Arrays.toString(excludedPermissions)); } + if (excludedPackages != null && excludedPackages.length > 0) { + pw.print(prefix); pw.print("excludedPackages="); + pw.print(Arrays.toString(excludedPackages)); + } if (options != null) { pw.print(prefix); pw.print("options="); pw.println(options.toBundle()); } @@ -260,7 +265,8 @@ final class BroadcastRecord extends Binder { Intent _intent, ProcessRecord _callerApp, String _callerPackage, @Nullable String _callerFeatureId, int _callingPid, int _callingUid, boolean _callerInstantApp, String _resolvedType, - String[] _requiredPermissions, String[] _excludedPermissions, int _appOp, + String[] _requiredPermissions, String[] _excludedPermissions, + String[] _excludedPackages, int _appOp, BroadcastOptions _options, List _receivers, IIntentReceiver _resultTo, int _resultCode, String _resultData, Bundle _resultExtras, boolean _serialized, boolean _sticky, boolean _initialSticky, int _userId, boolean allowBackgroundActivityStarts, @@ -280,6 +286,7 @@ final class BroadcastRecord extends Binder { resolvedType = _resolvedType; requiredPermissions = _requiredPermissions; excludedPermissions = _excludedPermissions; + excludedPackages = _excludedPackages; appOp = _appOp; options = _options; receivers = _receivers; @@ -321,6 +328,7 @@ final class BroadcastRecord extends Binder { resolvedType = from.resolvedType; requiredPermissions = from.requiredPermissions; excludedPermissions = from.excludedPermissions; + excludedPackages = from.excludedPackages; appOp = from.appOp; options = from.options; receivers = from.receivers; @@ -381,9 +389,10 @@ final class BroadcastRecord extends Binder { // build a new BroadcastRecord around that single-target list BroadcastRecord split = new BroadcastRecord(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType, - requiredPermissions, excludedPermissions, appOp, options, splitReceivers, resultTo, - resultCode, resultData, resultExtras, ordered, sticky, initialSticky, userId, - allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt); + requiredPermissions, excludedPermissions, excludedPackages, appOp, options, + splitReceivers, resultTo, resultCode, resultData, resultExtras, ordered, sticky, + initialSticky, userId, allowBackgroundActivityStarts, + mBackgroundActivityStartsToken, timeoutExempt); split.enqueueTime = this.enqueueTime; split.enqueueRealTime = this.enqueueRealTime; split.enqueueClockTime = this.enqueueClockTime; @@ -459,7 +468,7 @@ final class BroadcastRecord extends Binder { for (int i = 0; i < uidSize; i++) { final BroadcastRecord br = new BroadcastRecord(queue, intent, callerApp, callerPackage, callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType, - requiredPermissions, excludedPermissions, appOp, options, + requiredPermissions, excludedPermissions, excludedPackages, appOp, options, uid2receiverList.valueAt(i), null /* _resultTo */, resultCode, resultData, resultExtras, ordered, sticky, initialSticky, userId, allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt); diff --git a/services/core/java/com/android/server/am/ComponentAliasResolver.java b/services/core/java/com/android/server/am/ComponentAliasResolver.java index 2db3b15e719d..01735a754c83 100644 --- a/services/core/java/com/android/server/am/ComponentAliasResolver.java +++ b/services/core/java/com/android/server/am/ComponentAliasResolver.java @@ -483,7 +483,7 @@ public class ComponentAliasResolver { @Nullable public Resolution<ResolveInfo> resolveReceiver(@NonNull Intent intent, @NonNull ResolveInfo receiver, @Nullable String resolvedType, - int packageFlags, int userId, int callingUid) { + int packageFlags, int userId, int callingUid, boolean forSend) { // Resolve this alias. final Resolution<ComponentName> resolution = resolveComponentAlias(() -> receiver.activityInfo.getComponentName()); @@ -506,8 +506,8 @@ public class ComponentAliasResolver { i.setPackage(null); i.setComponent(resolution.getTarget()); - List<ResolveInfo> resolved = pmi.queryIntentReceivers(i, - resolvedType, packageFlags, callingUid, userId); + List<ResolveInfo> resolved = pmi.queryIntentReceivers( + i, resolvedType, packageFlags, callingUid, userId, forSend); if (resolved == null || resolved.size() == 0) { // Target component not found. Slog.w(TAG, "Alias target " + target.flattenToShortString() + " not found"); diff --git a/services/core/java/com/android/server/am/PreBootBroadcaster.java b/services/core/java/com/android/server/am/PreBootBroadcaster.java index 756209824614..35f91ba1169b 100644 --- a/services/core/java/com/android/server/am/PreBootBroadcaster.java +++ b/services/core/java/com/android/server/am/PreBootBroadcaster.java @@ -124,7 +124,7 @@ public abstract class PreBootBroadcaster extends IIntentReceiver.Stub { REASON_PRE_BOOT_COMPLETED, ""); synchronized (mService) { mService.broadcastIntentLocked(null, null, null, mIntent, null, this, 0, null, null, - null, null, AppOpsManager.OP_NONE, bOptions.toBundle(), true, + null, null, null, AppOpsManager.OP_NONE, bOptions.toBundle(), true, false, ActivityManagerService.MY_PID, Process.SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), mUserId); } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index c04377389e8e..7ffea26638f5 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -3203,8 +3203,8 @@ class UserController implements Handler.Callback { synchronized (mService) { return mService.broadcastIntentLocked(null, null, null, intent, resolvedType, resultTo, resultCode, resultData, resultExtras, requiredPermissions, null, - appOp, bOptions, ordered, sticky, callingPid, callingUid, realCallingUid, - realCallingPid, userId); + null, appOp, bOptions, ordered, sticky, callingPid, callingUid, + realCallingUid, realCallingPid, userId); } } diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java index d239c02d4529..27ce493f717f 100644 --- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java +++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java @@ -501,6 +501,7 @@ public final class AppHibernationService extends SystemService { null /* resultExtras */, requiredPermissions, null /* excludedPermissions */, + null /* excludedPackages */, OP_NONE, null /* bOptions */, false /* serialized */, @@ -519,6 +520,7 @@ public final class AppHibernationService extends SystemService { null /* resultExtras */, requiredPermissions, null /* excludedPermissions */, + null /* excludedPackages */, OP_NONE, null /* bOptions */, false /* serialized */, diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 36afb3677438..b9da144713cd 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -43,6 +43,7 @@ import static android.app.AppOpsManager.OP_FLAG_SELF; import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.OP_PLAY_AUDIO; +import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED; @@ -2368,7 +2369,8 @@ public class AppOpsService extends IAppOpsService.Stub { ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class); boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid(); if (!isSelfRequest) { - boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid()); + boolean isCallerInstrumented = + ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID; boolean isCallerSystem = Binder.getCallingPid() == Process.myPid(); boolean isCallerPermissionController; try { @@ -3848,7 +3850,7 @@ public class AppOpsService extends IAppOpsService.Stub { // the data gated by OP_RECORD_AUDIO. // // TODO: Revert this change before Android 12. - if (code == OP_RECORD_AUDIO_HOTWORD) { + if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) { int result = checkOperation(OP_RECORD_AUDIO, uid, packageName); if (result != AppOpsManager.MODE_ALLOWED) { return new SyncNotedAppOp(result, code, attributionTag, packageName); @@ -6924,7 +6926,8 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public @Nullable RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage() { ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class); - boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid()); + boolean isCallerInstrumented = + ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID; boolean isCallerSystem = Binder.getCallingPid() == Process.myPid(); if (!isCallerSystem && !isCallerInstrumented) { return null; diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java index 8de515d4d3e5..158092f33ee3 100644 --- a/services/core/java/com/android/server/appop/DiscreteRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java @@ -34,6 +34,7 @@ import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY; import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA; import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE; +import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.flagsToString; import static android.app.AppOpsManager.getUidStateName; @@ -133,7 +134,7 @@ final class DiscreteRegistry { private static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist"; private static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + OP_PHONE_CALL_MICROPHONE + "," - + OP_PHONE_CALL_CAMERA; + + OP_PHONE_CALL_CAMERA + "," + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis(); private static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis(); private static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION = diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index aed63ce5b2c6..0123c646c764 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -4146,19 +4146,8 @@ public class AudioService extends IAudioService.Stub { streamType = mStreamVolumeAlias[streamType]; - if (streamType == AudioSystem.STREAM_MUSIC) { - flags = updateFlagsForTvPlatform(flags); - synchronized (mHdmiClientLock) { - // Don't display volume UI on a TV Playback device when using absolute volume - if (mHdmiCecVolumeControlEnabled && mHdmiPlaybackClient != null - && (isAbsoluteVolumeDevice(device) - || isA2dpAbsoluteVolumeDevice(device))) { - flags &= ~AudioManager.FLAG_SHOW_UI; - } - } - if (isFullVolumeDevice(device)) { - flags &= ~AudioManager.FLAG_SHOW_UI; - } + if (streamType == AudioSystem.STREAM_MUSIC && isFullVolumeDevice(device)) { + flags &= ~AudioManager.FLAG_SHOW_UI; } mVolumeController.postVolumeChanged(streamType, flags); } @@ -6648,6 +6637,11 @@ public class AudioService extends IAudioService.Stub // verify arguments Objects.requireNonNull(device); AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior); + sVolumeLogger.log(new AudioEventLogger.StringEvent("setDeviceVolumeBehavior: dev:" + + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:" + + device.getAddress() + " behavior:" + + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior) + + " pack:" + pkgName).printLog(TAG)); if (pkgName == null) { pkgName = ""; } @@ -8363,7 +8357,7 @@ public class AudioService extends IAudioService.Stub private void avrcpSupportsAbsoluteVolume(String address, boolean support) { // address is not used for now, but may be used when multiple a2dp devices are supported sVolumeLogger.log(new AudioEventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr=" - + address + " support=" + support)); + + address + " support=" + support).printLog(TAG)); mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support); setAvrcpAbsoluteVolumeSupported(support); } diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 4ac611bb7e2a..0b9cb1977643 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -70,12 +70,6 @@ public class SpatializerHelper { private @Nullable SensorManager mSensorManager; //------------------------------------------------------------ - /** head tracker sensor name */ - // TODO: replace with generic head tracker sensor name. - // the current implementation refers to the "google" namespace but will be replaced - // by an android name at the next API level revision, it is not Google-specific. - private static final String HEADTRACKER_SENSOR = - "com.google.hardware.sensor.hid_dynamic.headtracker"; private static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(15) { { @@ -1522,24 +1516,24 @@ public class SpatializerHelper { private int getHeadSensorHandleUpdateTracker() { int headHandle = -1; UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(ROUTING_DEVICES[0]); - List<Sensor> sensors = new ArrayList<Sensor>(0); - sensors.addAll(mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER)); - sensors.addAll(mSensorManager.getDynamicSensorList(Sensor.TYPE_DEVICE_PRIVATE_BASE)); + // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion + // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR + // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by + // SensorPoseProvider). + // Note: this is a dynamic sensor list right now. + List<Sensor> sensors = mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER); for (Sensor sensor : sensors) { - if (sensor.getType() == Sensor.TYPE_HEAD_TRACKER - || sensor.getStringType().equals(HEADTRACKER_SENSOR)) { - UUID uuid = sensor.getUuid(); - if (uuid.equals(routingDeviceUuid)) { - headHandle = sensor.getHandle(); - if (!setHasHeadTracker(ROUTING_DEVICES[0])) { - headHandle = -1; - } - break; - } - if (uuid.equals(UuidUtils.STANDALONE_UUID)) { - headHandle = sensor.getHandle(); - break; + final UUID uuid = sensor.getUuid(); + if (uuid.equals(routingDeviceUuid)) { + headHandle = sensor.getHandle(); + if (!setHasHeadTracker(ROUTING_DEVICES[0])) { + headHandle = -1; } + break; + } + if (uuid.equals(UuidUtils.STANDALONE_UUID)) { + headHandle = sensor.getHandle(); + // we do not break, perhaps we find a head tracker on device. } } return headHandle; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java index de0a36a32b66..bf7a62aadc28 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java @@ -88,6 +88,10 @@ public class FaceResetLockoutClient extends HalClientMonitor<AidlSession> implem mCallback.onClientFinished(this, true /* success */); } + public boolean interruptsPrecedingClients() { + return true; + } + /** * Reset the local lockout state and notify any listeners. * diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java index 6e74d3622c1a..f29b9e823109 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java @@ -65,6 +65,10 @@ public class FaceResetLockoutClient extends HalClientMonitor<IBiometricsFace> { startHalOperation(); } + public boolean interruptsPrecedingClients() { + return true; + } + @Override protected void startHalOperation() { try { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java index f90cba79dac2..c8148df9ea71 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java @@ -82,6 +82,10 @@ class FingerprintResetLockoutClient extends HalClientMonitor<AidlSession> implem } } + public boolean interruptsPrecedingClients() { + return true; + } + void onLockoutCleared() { resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutCache, mLockoutResetDispatcher); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java index 559ca0633c42..843fcc8ee2a6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java @@ -50,6 +50,10 @@ public class FingerprintResetLockoutClient extends BaseClientMonitor { callback.onClientFinished(this, true /* success */); } + public boolean interruptsPrecedingClients() { + return true; + } + @Override public int getProtoEnum() { return BiometricsProto.CM_RESET_LOCKOUT; diff --git a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java index 0944c57d121f..ab3b2506f5ac 100644 --- a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java +++ b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java @@ -18,6 +18,7 @@ package com.android.server.clipboard; import android.annotation.Nullable; import android.content.ClipData; +import android.os.PersistableBundle; import android.os.SystemProperties; import android.system.ErrnoException; import android.system.Os; @@ -59,11 +60,11 @@ class EmulatorClipboardMonitor implements Consumer<ClipData> { return mPipe; } - private synchronized boolean openPipe() { - if (mPipe != null) { - return true; - } + private synchronized void setPipeFD(final FileDescriptor fd) { + mPipe = fd; + } + private static FileDescriptor openPipeImpl() { try { final FileDescriptor fd = Os.socket(OsConstants.AF_VSOCK, OsConstants.SOCK_STREAM, 0); @@ -71,39 +72,42 @@ class EmulatorClipboardMonitor implements Consumer<ClipData> { Os.connect(fd, new VmSocketAddress(HOST_PORT, OsConstants.VMADDR_CID_HOST)); final byte[] handshake = createOpenHandshake(); - Os.write(fd, handshake, 0, handshake.length); - mPipe = fd; - return true; + writeFully(fd, handshake, 0, handshake.length); + return fd; } catch (ErrnoException | SocketException | InterruptedIOException e) { Os.close(fd); } } catch (ErrnoException e) { } - return false; + return null; } - private synchronized void closePipe() { - try { - final FileDescriptor fd = mPipe; - mPipe = null; - if (fd != null) { - Os.close(fd); - } - } catch (ErrnoException ignore) { + private static FileDescriptor openPipe() throws InterruptedException { + FileDescriptor fd = openPipeImpl(); + + // There's no guarantee that QEMU pipes will be ready at the moment + // this method is invoked. We simply try to get the pipe open and + // retry on failure indefinitely. + while (fd == null) { + Thread.sleep(100); + fd = openPipeImpl(); } + + return fd; } - private byte[] receiveMessage() throws ErrnoException, InterruptedIOException, EOFException { + private static byte[] receiveMessage(final FileDescriptor fd) throws ErrnoException, + InterruptedIOException, EOFException { final byte[] lengthBits = new byte[4]; - readFully(mPipe, lengthBits, 0, lengthBits.length); + readFully(fd, lengthBits, 0, lengthBits.length); final ByteBuffer bb = ByteBuffer.wrap(lengthBits); bb.order(ByteOrder.LITTLE_ENDIAN); final int msgLen = bb.getInt(); final byte[] msg = new byte[msgLen]; - readFully(mPipe, msg, 0, msg.length); + readFully(fd, msg, 0, msg.length); return msg; } @@ -122,29 +126,40 @@ class EmulatorClipboardMonitor implements Consumer<ClipData> { EmulatorClipboardMonitor(final Consumer<ClipData> setAndroidClipboard) { this.mHostMonitorThread = new Thread(() -> { + FileDescriptor fd = null; + while (!Thread.interrupted()) { try { - // There's no guarantee that QEMU pipes will be ready at the moment - // this method is invoked. We simply try to get the pipe open and - // retry on failure indefinitely. - while (!openPipe()) { - Thread.sleep(100); + if (fd == null) { + fd = openPipe(); + setPipeFD(fd); } - final byte[] receivedData = receiveMessage(); + final byte[] receivedData = receiveMessage(fd); final String str = new String(receivedData); final ClipData clip = new ClipData("host clipboard", new String[]{"text/plain"}, new ClipData.Item(str)); + final PersistableBundle bundle = new PersistableBundle(); + bundle.putBoolean("com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY", true); + clip.getDescription().setExtras(bundle); if (LOG_CLIBOARD_ACCESS) { Slog.i(TAG, "Setting the guest clipboard to '" + str + "'"); } setAndroidClipboard.accept(clip); - } catch (ErrnoException | EOFException | InterruptedIOException e) { - closePipe(); - } catch (InterruptedException | IllegalArgumentException e) { + } catch (ErrnoException | EOFException | InterruptedIOException + | InterruptedException e) { + setPipeFD(null); + + try { + Os.close(fd); + } catch (ErrnoException e2) { + // ignore + } + + fd = null; } } }); @@ -154,34 +169,43 @@ class EmulatorClipboardMonitor implements Consumer<ClipData> { @Override public void accept(final @Nullable ClipData clip) { - if (clip == null) { - setHostClipboardImpl(""); - } else if (clip.getItemCount() > 0) { - final CharSequence text = clip.getItemAt(0).getText(); - if (text != null) { - setHostClipboardImpl(text.toString()); - } + final FileDescriptor fd = getPipeFD(); + if (fd != null) { + setHostClipboard(fd, getClipString(clip)); } } - private void setHostClipboardImpl(final String value) { - final FileDescriptor pipeFD = getPipeFD(); + private String getClipString(final @Nullable ClipData clip) { + if (clip == null) { + return ""; + } - if (pipeFD != null) { - Thread t = new Thread(() -> { - if (LOG_CLIBOARD_ACCESS) { - Slog.i(TAG, "Setting the host clipboard to '" + value + "'"); - } + if (clip.getItemCount() == 0) { + return ""; + } - try { - sendMessage(pipeFD, value.getBytes()); - } catch (ErrnoException | InterruptedIOException e) { - Slog.e(TAG, "Failed to set host clipboard " + e.getMessage()); - } catch (IllegalArgumentException e) { - } - }); - t.start(); + final CharSequence text = clip.getItemAt(0).getText(); + if (text == null) { + return ""; } + + return text.toString(); + } + + private static void setHostClipboard(final FileDescriptor fd, final String value) { + Thread t = new Thread(() -> { + if (LOG_CLIBOARD_ACCESS) { + Slog.i(TAG, "Setting the host clipboard to '" + value + "'"); + } + + try { + sendMessage(fd, value.getBytes()); + } catch (ErrnoException | InterruptedIOException e) { + Slog.e(TAG, "Failed to set host clipboard " + e.getMessage()); + } catch (IllegalArgumentException e) { + } + }); + t.start(); } private static void readFully(final FileDescriptor fd, diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 68a53f13da8a..089a92402de5 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -27,6 +27,8 @@ import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN; import static android.os.PowerWhitelistManager.REASON_VPN; import static android.os.UserHandle.PER_USER_RANGE; +import static com.android.server.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER; + import static java.util.Objects.requireNonNull; import android.Manifest; @@ -97,6 +99,7 @@ import android.os.INetworkManagementService; import android.os.Looper; import android.os.Parcel; import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -127,6 +130,7 @@ import com.android.net.module.util.NetworkStackConstants; import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; import com.android.server.net.BaseNetworkObserver; +import com.android.server.vcn.util.PersistableBundleUtils; import libcore.io.IoUtils; @@ -174,6 +178,8 @@ public class Vpn { private static final String VPN_PROVIDER_NAME_BASE = "VpnNetworkProvider:"; private static final boolean LOGD = true; private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore"; + /** Key containing prefix of vpn app excluded list */ + @VisibleForTesting static final String VPN_APP_EXCLUDED = "VPN_APP_EXCLUDED_"; // Length of time (in milliseconds) that an app hosting an always-on VPN is placed on // the device idle allowlist during service launch and VPN bootstrap. @@ -209,7 +215,6 @@ public class Vpn { private final NetworkInfo mNetworkInfo; private int mLegacyState; @VisibleForTesting protected String mPackage; - private String mSessionKey; private int mOwnerUID; private boolean mIsPackageTargetingAtLeastQ; @VisibleForTesting @@ -1534,11 +1539,17 @@ public class Vpn { } // Note: Return type guarantees results are deduped and sorted, which callers require. + // This method also adds the SDK sandbox UIDs corresponding to the applications by default, + // since apps are generally not aware of them, yet they should follow the VPN configuration + // of the app they belong to. private SortedSet<Integer> getAppsUids(List<String> packageNames, int userId) { SortedSet<Integer> uids = new TreeSet<>(); for (String app : packageNames) { int uid = getAppUid(app, userId); if (uid != -1) uids.add(uid); + if (Process.isApplicationUid(uid)) { + uids.add(Process.toSdkSandboxUid(uid)); + } } return uids; } @@ -1991,9 +2002,7 @@ public class Vpn { public synchronized int getActiveVpnType() { if (!mNetworkInfo.isConnectedOrConnecting()) return VpnManager.TYPE_VPN_NONE; if (mVpnRunner == null) return VpnManager.TYPE_VPN_SERVICE; - return mVpnRunner instanceof IkeV2VpnRunner - ? VpnManager.TYPE_VPN_PLATFORM - : VpnManager.TYPE_VPN_LEGACY; + return isIkev2VpnRunner() ? VpnManager.TYPE_VPN_PLATFORM : VpnManager.TYPE_VPN_LEGACY; } private void updateAlwaysOnNotification(DetailedState networkState) { @@ -2531,12 +2540,15 @@ public class Vpn { @Nullable private IpSecTunnelInterface mTunnelIface; @Nullable private IkeSession mSession; @Nullable private Network mActiveNetwork; + private final String mSessionKey; IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) { super(TAG); mProfile = profile; mIpSecManager = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); - mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this); + // Pass mExecutor into Ikev2VpnNetworkCallback and make sure that IkeV2VpnRunnerCallback + // will be called by the mExecutor thread. + mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this, mExecutor); mSessionKey = UUID.randomUUID().toString(); } @@ -2630,6 +2642,8 @@ public class Vpn { mConfig.underlyingNetworks = new Network[] {network}; + mConfig.disallowedApplications = getAppExclusionList(mPackage); + networkAgent = mNetworkAgent; // The below must be done atomically with the mConfig update, otherwise @@ -2691,73 +2705,68 @@ public class Vpn { * <p>The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE * state in the process, and starting a new IkeSession instance. * - * <p>This method is called multiple times over the lifetime of the Ikev2VpnRunner, and is - * called on the ConnectivityService thread. Thus, the actual work MUST be proxied to the - * mExecutor thread in order to ensure consistency of the Ikev2VpnRunner fields. + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. */ public void onDefaultNetworkChanged(@NonNull Network network) { Log.d(TAG, "Starting IKEv2/IPsec session on new network: " + network); - // Proxy to the Ikev2VpnRunner (single-thread) executor to ensure consistency in lieu - // of locking. - mExecutor.execute(() -> { - try { - if (!mIsRunning) { - Log.d(TAG, "onDefaultNetworkChanged after exit"); - return; // VPN has been shut down. - } + try { + if (!mIsRunning) { + Log.d(TAG, "onDefaultNetworkChanged after exit"); + return; // VPN has been shut down. + } - // Clear mInterface to prevent Ikev2VpnRunner being cleared when - // interfaceRemoved() is called. - mInterface = null; - // Without MOBIKE, we have no way to seamlessly migrate. Close on old - // (non-default) network, and start the new one. - resetIkeState(); - mActiveNetwork = network; - - // Get Ike options from IkeTunnelConnectionParams if it's available in the - // profile. - final IkeTunnelConnectionParams ikeTunConnParams = - mProfile.getIkeTunnelConnectionParams(); - final IkeSessionParams ikeSessionParams; - final ChildSessionParams childSessionParams; - if (ikeTunConnParams != null) { - final IkeSessionParams.Builder builder = new IkeSessionParams.Builder( - ikeTunConnParams.getIkeSessionParams()).setNetwork(network); - ikeSessionParams = builder.build(); - childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams(); - } else { - ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams( - mContext, mProfile, network); - childSessionParams = VpnIkev2Utils.buildChildSessionParams( - mProfile.getAllowedAlgorithms()); - } + // Clear mInterface to prevent Ikev2VpnRunner being cleared when + // interfaceRemoved() is called. + mInterface = null; + // Without MOBIKE, we have no way to seamlessly migrate. Close on old + // (non-default) network, and start the new one. + resetIkeState(); + mActiveNetwork = network; - // TODO: Remove the need for adding two unused addresses with - // IPsec tunnels. - final InetAddress address = InetAddress.getLocalHost(); - mTunnelIface = - mIpSecManager.createIpSecTunnelInterface( - address /* unused */, - address /* unused */, - network); - NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName()); - - mSession = mIkev2SessionCreator.createIkeSession( - mContext, - ikeSessionParams, - childSessionParams, - mExecutor, - new VpnIkev2Utils.IkeSessionCallbackImpl( - TAG, IkeV2VpnRunner.this, network), - new VpnIkev2Utils.ChildSessionCallbackImpl( - TAG, IkeV2VpnRunner.this, network)); - Log.d(TAG, "Ike Session started for network " + network); - } catch (Exception e) { - Log.i(TAG, "Setup failed for network " + network + ". Aborting", e); - onSessionLost(network, e); + // Get Ike options from IkeTunnelConnectionParams if it's available in the + // profile. + final IkeTunnelConnectionParams ikeTunConnParams = + mProfile.getIkeTunnelConnectionParams(); + final IkeSessionParams ikeSessionParams; + final ChildSessionParams childSessionParams; + if (ikeTunConnParams != null) { + final IkeSessionParams.Builder builder = new IkeSessionParams.Builder( + ikeTunConnParams.getIkeSessionParams()).setNetwork(network); + ikeSessionParams = builder.build(); + childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams(); + } else { + ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams( + mContext, mProfile, network); + childSessionParams = VpnIkev2Utils.buildChildSessionParams( + mProfile.getAllowedAlgorithms()); } - }); + + // TODO: Remove the need for adding two unused addresses with + // IPsec tunnels. + final InetAddress address = InetAddress.getLocalHost(); + mTunnelIface = + mIpSecManager.createIpSecTunnelInterface( + address /* unused */, + address /* unused */, + network); + NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName()); + + mSession = mIkev2SessionCreator.createIkeSession( + mContext, + ikeSessionParams, + childSessionParams, + mExecutor, + new VpnIkev2Utils.IkeSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, network), + new VpnIkev2Utils.ChildSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, network)); + Log.d(TAG, "Ike Session started for network " + network); + } catch (Exception e) { + Log.i(TAG, "Setup failed for network " + network + ". Aborting", e); + onSessionLost(network, e); + } } /** Marks the state as FAILED, and disconnects. */ @@ -2876,7 +2885,6 @@ public class Vpn { */ private void disconnectVpnRunner() { mActiveNetwork = null; - mSessionKey = null; mIsRunning = false; resetIkeState(); @@ -3306,7 +3314,7 @@ public class Vpn { } private boolean isCurrentIkev2VpnLocked(@NonNull String packageName) { - return isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner; + return isCurrentPreparedPackage(packageName) && isIkev2VpnRunner(); } /** @@ -3360,6 +3368,16 @@ public class Vpn { return VpnProfile.decode("" /* Key unused */, encoded); } + private boolean isIkev2VpnRunner() { + return (mVpnRunner instanceof IkeV2VpnRunner); + } + + @GuardedBy("this") + @Nullable + private String getSessionKeyLocked() { + return isIkev2VpnRunner() ? ((IkeV2VpnRunner) mVpnRunner).mSessionKey : null; + } + /** * Starts an already provisioned VPN Profile, keyed by package name. * @@ -3387,7 +3405,11 @@ public class Vpn { } startVpnProfilePrivileged(profile, packageName); - return mSessionKey; + if (!isIkev2VpnRunner()) { + throw new IllegalStateException("mVpnRunner shouldn't be null and should also be " + + "an instance of Ikev2VpnRunner"); + } + return getSessionKeyLocked(); } finally { Binder.restoreCallingIdentity(token); } @@ -3472,6 +3494,88 @@ public class Vpn { } } + private boolean storeAppExclusionList(@NonNull String packageName, + @NonNull List<String> excludedApps) { + byte[] data; + try { + final PersistableBundle bundle = PersistableBundleUtils.fromList( + excludedApps, PersistableBundleUtils.STRING_SERIALIZER); + data = PersistableBundleUtils.toDiskStableBytes(bundle); + } catch (IOException e) { + Log.e(TAG, "problem writing into stream", e); + return false; + } + + final long oldId = Binder.clearCallingIdentity(); + try { + getVpnProfileStore().put(getVpnAppExcludedForPackage(packageName), data); + } finally { + Binder.restoreCallingIdentity(oldId); + } + return true; + } + + @VisibleForTesting + String getVpnAppExcludedForPackage(String packageName) { + return VPN_APP_EXCLUDED + mUserId + "_" + packageName; + } + + /** + * Set the application exclusion list for the specified VPN profile. + * + * @param packageName the package name of the app provisioning this profile + * @param excludedApps the list of excluded packages + * + * @return whether setting the list is successful or not + */ + public synchronized boolean setAppExclusionList(@NonNull String packageName, + @NonNull List<String> excludedApps) { + enforceNotRestrictedUser(); + if (!storeAppExclusionList(packageName, excludedApps)) return false; + // Re-build and update NetworkCapabilities via NetworkAgent. + if (mNetworkAgent != null) { + // Only update the platform VPN + if (isIkev2VpnRunner()) { + mConfig.disallowedApplications = List.copyOf(excludedApps); + mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities) + .setUids(createUserAndRestrictedProfilesRanges( + mUserId, null /* allowedApplications */, excludedApps)) + .build(); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + } + + return true; + } + + /** + * Gets the application exclusion list for the specified VPN profile. + * + * @param packageName the package name of the app provisioning this profile + * @return the list of excluded packages for the specified VPN profile or empty list if there is + * no provisioned VPN profile. + */ + @NonNull + public synchronized List<String> getAppExclusionList(@NonNull String packageName) { + enforceNotRestrictedUser(); + + final long oldId = Binder.clearCallingIdentity(); + try { + final byte[] bytes = getVpnProfileStore().get(getVpnAppExcludedForPackage(packageName)); + + if (bytes == null || bytes.length == 0) return new ArrayList<>(); + + final PersistableBundle bundle = PersistableBundleUtils.fromDiskStableBytes(bytes); + return PersistableBundleUtils.toList(bundle, STRING_DESERIALIZER); + } catch (IOException e) { + Log.e(TAG, "problem reading from stream", e); + } finally { + Binder.restoreCallingIdentity(oldId); + } + + return new ArrayList<>(); + } + private @VpnProfileState.State int getStateFromLegacyState(int legacyState) { switch (legacyState) { case LegacyVpnInfo.STATE_CONNECTING: @@ -3490,11 +3594,8 @@ public class Vpn { } private VpnProfileState makeVpnProfileState() { - // TODO: mSessionKey will be moved to Ikev2VpnRunner once aosp/2007077 is merged, so after - // merging aosp/2007077, here should check Ikev2VpnRunner is null or not. Session key will - // be null if Ikev2VpnRunner is null. - return new VpnProfileState(getStateFromLegacyState(mLegacyState), mSessionKey, mAlwaysOn, - mLockdown); + return new VpnProfileState(getStateFromLegacyState(mLegacyState), + isIkev2VpnRunner() ? getSessionKeyLocked() : null, mAlwaysOn, mLockdown); } /** diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java index a0a596d998bf..6982d6095689 100644 --- a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java +++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java @@ -86,6 +86,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.concurrent.ExecutorService; /** * Utility class to build and convert IKEv2/IPsec parameters. @@ -376,22 +377,25 @@ public class VpnIkev2Utils { static class Ikev2VpnNetworkCallback extends NetworkCallback { private final String mTag; private final Vpn.IkeV2VpnRunnerCallback mCallback; + private final ExecutorService mExecutor; - Ikev2VpnNetworkCallback(String tag, Vpn.IkeV2VpnRunnerCallback callback) { + Ikev2VpnNetworkCallback(String tag, Vpn.IkeV2VpnRunnerCallback callback, + ExecutorService executor) { mTag = tag; mCallback = callback; + mExecutor = executor; } @Override public void onAvailable(@NonNull Network network) { Log.d(mTag, "Starting IKEv2/IPsec session on new network: " + network); - mCallback.onDefaultNetworkChanged(network); + mExecutor.execute(() -> mCallback.onDefaultNetworkChanged(network)); } @Override public void onLost(@NonNull Network network) { Log.d(mTag, "Tearing down; lost network: " + network); - mCallback.onSessionLost(network, null); + mExecutor.execute(() -> mCallback.onSessionLost(network, null)); } } diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index 80355262be0e..8de150ac4124 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -948,19 +948,15 @@ public final class ColorDisplayService extends SystemService { if (!isColorModeAvailable(colorMode)) { final int[] mappedColorModes = getContext().getResources().getIntArray( R.array.config_mappedColorModes); - if (colorMode == COLOR_MODE_BOOSTED && mappedColorModes.length > COLOR_MODE_NATURAL - && isColorModeAvailable(mappedColorModes[COLOR_MODE_NATURAL])) { - colorMode = COLOR_MODE_NATURAL; - } else if (colorMode == COLOR_MODE_SATURATED - && mappedColorModes.length > COLOR_MODE_AUTOMATIC - && isColorModeAvailable(mappedColorModes[COLOR_MODE_AUTOMATIC])) { - colorMode = COLOR_MODE_AUTOMATIC; - } else if (colorMode == COLOR_MODE_AUTOMATIC - && mappedColorModes.length > COLOR_MODE_SATURATED - && isColorModeAvailable(mappedColorModes[COLOR_MODE_SATURATED])) { - colorMode = COLOR_MODE_SATURATED; + if (colorMode != -1 && mappedColorModes.length > colorMode + && isColorModeAvailable(mappedColorModes[colorMode])) { + colorMode = mappedColorModes[colorMode]; } else { - colorMode = -1; + final int[] availableColorModes = getContext().getResources().getIntArray( + R.array.config_availableColorModes); + if (availableColorModes.length > 0) { + colorMode = availableColorModes[0]; + } } } diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 4e1d899b26a6..63c5456c972b 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -124,8 +124,10 @@ public final class DreamManagerService extends SystemService { final boolean activityAllowed = activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_DREAM || activityType == ACTIVITY_TYPE_ASSISTANT; - if (mCurrentDreamToken != null && !mCurrentDreamIsWaking && !activityAllowed) { - stopDreamInternal(false, "activity starting: " + activityInfo.name); + if (mCurrentDreamToken != null && !mCurrentDreamIsWaking + && !mCurrentDreamIsDozing && !activityAllowed) { + requestAwakenInternal( + "stopping dream due to activity start: " + activityInfo.name); } } }; @@ -229,13 +231,13 @@ public final class DreamManagerService extends SystemService { mPowerManager.nap(time); } - private void requestAwakenInternal() { + private void requestAwakenInternal(String reason) { // Treat an explicit request to awaken as user activity so that the // device doesn't immediately go to sleep if the timeout expired, // for example when being undocked. long time = SystemClock.uptimeMillis(); mPowerManager.userActivity(time, false /*noChangeLights*/); - stopDreamInternal(false /*immediate*/, "request awaken"); + stopDreamInternal(false /*immediate*/, reason); } private void finishSelfInternal(IBinder token, boolean immediate) { @@ -715,7 +717,7 @@ public final class DreamManagerService extends SystemService { final long ident = Binder.clearCallingIdentity(); try { - requestAwakenInternal(); + requestAwakenInternal("request awaken"); } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 1ea1457439ec..9bce471fd0cb 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -810,35 +810,24 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { } } - /** - * Change ARC status into the given {@code enabled} status. - * - * @return {@code true} if ARC was in "Enabled" status - */ @ServiceThreadOnly - boolean setArcStatus(boolean enabled) { + void enableArc(List<byte[]> supportedSads) { assertRunOnServiceThread(); + HdmiLogger.debug("Set Arc Status[old:%b new:true]", mArcEstablished); - HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled); - boolean oldStatus = mArcEstablished; - if (enabled) { - RequestSadAction action = new RequestSadAction( - this, Constants.ADDR_AUDIO_SYSTEM, - new RequestSadAction.RequestSadCallback() { - @Override - public void onRequestSadDone(List<byte[]> supportedSads) { - enableAudioReturnChannel(enabled); - notifyArcStatusToAudioService(enabled, supportedSads); - mArcEstablished = enabled; - } - }); - addAndStartAction(action); - } else { - enableAudioReturnChannel(enabled); - notifyArcStatusToAudioService(enabled, new ArrayList<>()); - mArcEstablished = enabled; - } - return oldStatus; + enableAudioReturnChannel(true); + notifyArcStatusToAudioService(true, supportedSads); + mArcEstablished = true; + } + + @ServiceThreadOnly + void disableArc() { + assertRunOnServiceThread(); + HdmiLogger.debug("Set Arc Status[old:%b new:false]", mArcEstablished); + + enableAudioReturnChannel(false); + notifyArcStatusToAudioService(false, new ArrayList<>()); + mArcEstablished = false; } /** @@ -1066,7 +1055,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { protected int handleTerminateArc(HdmiCecMessage message) { assertRunOnServiceThread(); if (mService .isPowerStandbyOrTransient()) { - setArcStatus(false); + disableArc(); return Constants.HANDLED; } // Do not check ARC configuration since the AVR might have been already removed. @@ -1353,7 +1342,7 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { if (avr == null) { return; } - setArcStatus(false); + disableArc(); // Seq #44. removeAllRunningArcAction(); diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 9824b4e6c43a..f8a74f4f3f55 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -4176,7 +4176,11 @@ public class HdmiControlService extends SystemService { List<AudioDeviceAttributes> streamMusicDevices = getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES); if (streamMusicDevices.contains(getAvcAudioOutputDevice())) { - setStreamMusicVolume(volume, AudioManager.FLAG_ABSOLUTE_VOLUME); + int flags = AudioManager.FLAG_ABSOLUTE_VOLUME; + if (isTvDevice()) { + flags |= AudioManager.FLAG_SHOW_UI; + } + setStreamMusicVolume(volume, flags); } } @@ -4190,8 +4194,11 @@ public class HdmiControlService extends SystemService { getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES); if (streamMusicDevices.contains(getAvcAudioOutputDevice())) { int direction = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE; - getAudioManager().adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, - AudioManager.FLAG_ABSOLUTE_VOLUME); + int flags = AudioManager.FLAG_ABSOLUTE_VOLUME; + if (isTvDevice()) { + flags |= AudioManager.FLAG_SHOW_UI; + } + getAudioManager().adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, flags); } } diff --git a/services/core/java/com/android/server/hdmi/RequestArcAction.java b/services/core/java/com/android/server/hdmi/RequestArcAction.java index c70101c43d79..3d9a2905f4fb 100644 --- a/services/core/java/com/android/server/hdmi/RequestArcAction.java +++ b/services/core/java/com/android/server/hdmi/RequestArcAction.java @@ -63,7 +63,7 @@ abstract class RequestArcAction extends HdmiCecFeatureAction { finish(); return true; } else if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_INITIATION) { - tv().setArcStatus(false); + tv().disableArc(); finish(); return true; } diff --git a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java index 4eb220fd65ee..3b7f1dd0d971 100644 --- a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java +++ b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java @@ -48,7 +48,7 @@ final class RequestArcInitiationAction extends RequestArcAction { public void onSendCompleted(int error) { if (error != SendMessageResult.SUCCESS) { // Turn off ARC status if <Request ARC Initiation> fails. - tv().setArcStatus(false); + tv().disableArc(); finish(); } } diff --git a/services/core/java/com/android/server/hdmi/RequestSadAction.java b/services/core/java/com/android/server/hdmi/RequestSadAction.java index 702c0004cb91..23aaf3260bac 100644 --- a/services/core/java/com/android/server/hdmi/RequestSadAction.java +++ b/services/core/java/com/android/server/hdmi/RequestSadAction.java @@ -181,13 +181,20 @@ final class RequestSadAction extends HdmiCecFeatureAction { return true; } if (cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT - && (cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_REQUEST_SHORT_AUDIO_DESCRIPTOR - && (cmd.getParams()[1] & 0xFF) == Constants.ABORT_INVALID_OPERAND) { - // Queried SADs are not supported - mQueriedSadCount += MAX_SAD_PER_REQUEST; - mTimeoutRetry = 0; - querySad(); - return true; + && (cmd.getParams()[0] & 0xFF) + == Constants.MESSAGE_REQUEST_SHORT_AUDIO_DESCRIPTOR) { + if ((cmd.getParams()[1] & 0xFF) == Constants.ABORT_UNRECOGNIZED_OPCODE) { + // SAD feature is not supported + wrapUpAndFinish(); + return true; + } + if ((cmd.getParams()[1] & 0xFF) == Constants.ABORT_INVALID_OPERAND) { + // Queried SADs are not supported + mQueriedSadCount += MAX_SAD_PER_REQUEST; + mTimeoutRetry = 0; + querySad(); + return true; + } } return false; } @@ -211,9 +218,9 @@ final class RequestSadAction extends HdmiCecFeatureAction { querySad(); return; } - mQueriedSadCount += MAX_SAD_PER_REQUEST; - mTimeoutRetry = 0; - querySad(); + // Don't query any other SADs if one of the SAD queries ran into the maximum amount of + // retries. + wrapUpAndFinish(); } } diff --git a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java index db93ad0617ff..32e274ece9ab 100644 --- a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java +++ b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java @@ -20,6 +20,8 @@ import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.tv.cec.V1_0.SendMessageResult; import android.util.Slog; +import java.util.List; + /** * Feature action that handles enabling/disabling of ARC transmission channel. * Once TV gets <Initiate ARC>, TV sends <Report ARC Initiated> to AV Receiver. @@ -55,21 +57,31 @@ final class SetArcTransmissionStateAction extends HdmiCecFeatureAction { boolean start() { // Seq #37. if (mEnabled) { - // Enable ARC status immediately before sending <Report Arc Initiated>. - // If AVR responds with <Feature Abort>, disable ARC status again. - // This is different from spec that says that turns ARC status to - // "Enabled" if <Report ARC Initiated> is acknowledged and no - // <Feature Abort> is received. - // But implemented this way to save the time having to wait for - // <Feature Abort>. - setArcStatus(true); - // If succeeds to send <Report ARC Initiated>, wait general timeout - // to check whether there is no <Feature Abort> for <Report ARC Initiated>. - mState = STATE_WAITING_TIMEOUT; - addTimer(mState, HdmiConfig.TIMEOUT_MS); - sendReportArcInitiated(); + // Request SADs before enabling ARC + RequestSadAction action = new RequestSadAction( + localDevice(), Constants.ADDR_AUDIO_SYSTEM, + new RequestSadAction.RequestSadCallback() { + @Override + public void onRequestSadDone(List<byte[]> supportedSads) { + // Enable ARC status immediately before sending <Report Arc Initiated>. + // If AVR responds with <Feature Abort>, disable ARC status again. + // This is different from spec that says that turns ARC status to + // "Enabled" if <Report ARC Initiated> is acknowledged and no + // <Feature Abort> is received. + // But implemented this way to save the time having to wait for + // <Feature Abort>. + Slog.i(TAG, "Enabling ARC"); + tv().enableArc(supportedSads); + // If succeeds to send <Report ARC Initiated>, wait general timeout to + // check whether there is no <Feature Abort> for <Report ARC Initiated>. + mState = STATE_WAITING_TIMEOUT; + addTimer(mState, HdmiConfig.TIMEOUT_MS); + sendReportArcInitiated(); + } + }); + addAndStartAction(action); } else { - setArcStatus(false); + disableArc(); finish(); } return true; @@ -92,7 +104,7 @@ final class SetArcTransmissionStateAction extends HdmiCecFeatureAction { case SendMessageResult.NACK: // If <Report ARC Initiated> is negatively ack'ed, disable ARC and // send <Report ARC Terminated> directly. - setArcStatus(false); + disableArc(); HdmiLogger.debug("Failed to send <Report Arc Initiated>."); finish(); break; @@ -101,16 +113,12 @@ final class SetArcTransmissionStateAction extends HdmiCecFeatureAction { }); } - private void setArcStatus(boolean enabled) { - tv().setArcStatus(enabled); - Slog.i(TAG, "Change arc status to " + enabled); + private void disableArc() { + Slog.i(TAG, "Disabling ARC"); - // If enabled before and set to "disabled" and send <Report Arc Terminated> to - // av reciever. - if (!enabled) { - sendCommand(HdmiCecMessageBuilder.buildReportArcTerminated(getSourceAddress(), - mAvrAddress)); - } + tv().disableArc(); + sendCommand(HdmiCecMessageBuilder.buildReportArcTerminated(getSourceAddress(), + mAvrAddress)); } @Override @@ -124,7 +132,7 @@ final class SetArcTransmissionStateAction extends HdmiCecFeatureAction { int originalOpcode = cmd.getParams()[0] & 0xFF; if (originalOpcode == Constants.MESSAGE_REPORT_ARC_INITIATED) { HdmiLogger.debug("Feature aborted for <Report Arc Initiated>"); - setArcStatus(false); + disableArc(); finish(); return true; } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index e4e9d1d49a6e..b624d438a4f7 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -20,6 +20,7 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManagerInternal; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -860,6 +861,19 @@ public class InputManagerService extends IInputManager.Stub @Override // Binder call public boolean injectInputEvent(InputEvent event, int mode) { + return injectInputEventToTarget(event, mode, Process.INVALID_UID); + } + + @Override // Binder call + public boolean injectInputEventToTarget(InputEvent event, int mode, int targetUid) { + if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS, + "injectInputEvent()", true /*checkInstrumentationSource*/)) { + throw new SecurityException( + "Injecting input events requires the caller (or the source of the " + + "instrumentation, if any) to have the INJECT_EVENTS permission."); + } + // We are not checking if targetUid matches the callingUid, since having the permission + // already means you can inject into any window. Objects.requireNonNull(event, "event must not be null"); if (mode != InputEventInjectionSync.NONE && mode != InputEventInjectionSync.WAIT_FOR_FINISHED @@ -868,22 +882,39 @@ public class InputManagerService extends IInputManager.Stub } final int pid = Binder.getCallingPid(); - final int uid = Binder.getCallingUid(); final long ident = Binder.clearCallingIdentity(); + final boolean injectIntoUid = targetUid != Process.INVALID_UID; final int result; try { - result = mNative.injectInputEvent(event, pid, uid, mode, - INJECTION_TIMEOUT_MILLIS, WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT); + result = mNative.injectInputEvent(event, injectIntoUid, + targetUid, mode, INJECTION_TIMEOUT_MILLIS, + WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT); } finally { Binder.restoreCallingIdentity(ident); } switch (result) { - case InputEventInjectionResult.PERMISSION_DENIED: - Slog.w(TAG, "Input event injection from pid " + pid + " permission denied."); - throw new SecurityException( - "Injecting to another application requires INJECT_EVENTS permission"); case InputEventInjectionResult.SUCCEEDED: return true; + case InputEventInjectionResult.TARGET_MISMATCH: + if (!injectIntoUid) { + throw new IllegalStateException("Injection should not result in TARGET_MISMATCH" + + " when it is not targeted into to a specific uid."); + } + // TODO(b/228161340): Remove the fallback of targeting injection into all windows + // when the caller has the injection permission. + // Explicitly maintain the same behavior as previous versions of Android, where + // injection is allowed into all windows if the caller has the INJECT_EVENTS + // permission, even if it is targeting a certain uid. + if (checkCallingPermission(android.Manifest.permission.INJECT_EVENTS, + "injectInputEvent-target-mismatch-fallback")) { + Slog.w(TAG, "Targeted input event was not directed at a window owned by uid " + + targetUid + ". Falling back to injecting into all windows."); + return injectInputEventToTarget(event, mode, Process.INVALID_UID); + } + throw new IllegalArgumentException( + "Targeted input event injection from pid " + pid + + " was not directed at a window owned by uid " + + targetUid + "."); case InputEventInjectionResult.TIMED_OUT: Slog.w(TAG, "Input event injection from pid " + pid + " timed out."); return false; @@ -2780,8 +2811,12 @@ public class InputManagerService extends IInputManager.Stub } } } - private boolean checkCallingPermission(String permission, String func) { + return checkCallingPermission(permission, func, false /*checkInstrumentationSource*/); + } + + private boolean checkCallingPermission(String permission, String func, + boolean checkInstrumentationSource) { // Quick check: if the calling permission is me, it's all okay. if (Binder.getCallingPid() == Process.myPid()) { return true; @@ -2790,6 +2825,28 @@ public class InputManagerService extends IInputManager.Stub if (mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) { return true; } + + if (checkInstrumentationSource) { + final ActivityManagerInternal ami = + LocalServices.getService(ActivityManagerInternal.class); + Objects.requireNonNull(ami, "ActivityManagerInternal should not be null."); + final int instrumentationUid = ami.getInstrumentationSourceUid(Binder.getCallingUid()); + if (instrumentationUid != Process.INVALID_UID) { + // Clear the calling identity when checking if the instrumentation source has + // permission because PackageManager will deny all permissions to some callers, + // such as instant apps. + final long token = Binder.clearCallingIdentity(); + try { + if (mContext.checkPermission(permission, -1 /*pid*/, instrumentationUid) + == PackageManager.PERMISSION_GRANTED) { + return true; + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + String msg = "Permission Denial: " + func + " from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() @@ -3035,13 +3092,6 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") - private boolean checkInjectEventsPermission(int injectorPid, int injectorUid) { - return mContext.checkPermission(android.Manifest.permission.INJECT_EVENTS, - injectorPid, injectorUid) == PackageManager.PERMISSION_GRANTED; - } - - // Native callback. - @SuppressWarnings("unused") private void onPointerDownOutsideFocus(IBinder touchedToken) { mWindowManagerCallbacks.onPointerDownOutsideFocus(touchedToken); } @@ -3501,12 +3551,17 @@ public class InputManagerService extends IInputManager.Stub @Override public void sendInputEvent(InputEvent event, int policyFlags) { + if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS, + "sendInputEvent()")) { + throw new SecurityException( + "The INJECT_EVENTS permission is required for injecting input events."); + } Objects.requireNonNull(event, "event must not be null"); synchronized (mInputFilterLock) { if (!mDisconnected) { - mNative.injectInputEvent(event, 0, 0, - InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0, + mNative.injectInputEvent(event, false /* injectIntoUid */, -1 /* uid */, + InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0 /* timeout */, policyFlags | WindowManagerPolicy.FLAG_FILTERED); } } diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 81882d277a99..9cf80737e67d 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -70,7 +70,18 @@ public interface NativeInputManagerService { void setBlockUntrustedTouchesMode(int mode); - int injectInputEvent(InputEvent event, int pid, int uid, int syncMode, + /** + * Inject an input event into the system. + * + * @param event the input event to inject + * @param injectIntoUid true if the event should target windows owned by uid, false otherwise + * @param uid the uid whose windows should be targeted, if any + * @param syncMode {@link android.os.InputEventInjectionSync} + * @param timeoutMillis timeout to wait for input injection to complete, in milliseconds + * @param policyFlags defined in {@link android.view.WindowManagerPolicyConstants} + * @return {@link android.os.InputEventInjectionResult} + */ + int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid, int syncMode, int timeoutMillis, int policyFlags); VerifiedInputEvent verifyInputEvent(InputEvent event); @@ -240,7 +251,8 @@ public interface NativeInputManagerService { public native void setBlockUntrustedTouchesMode(int mode); @Override - public native int injectInputEvent(InputEvent event, int pid, int uid, int syncMode, + public native int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid, + int syncMode, int timeoutMillis, int policyFlags); @Override diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java index ea99e7972887..f5c2bbc8d5a2 100644 --- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java +++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java @@ -1587,6 +1587,7 @@ public class GnssLocationProvider extends AbstractLocationProvider implements if (isGpsEnabled()) { setGpsEnabled(false); updateEnabled(); + restartLocationRequest(); } } diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java index 718f98a0f04b..70b86898c24b 100644 --- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java +++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java @@ -200,7 +200,7 @@ class GnssNetworkConnectivityHandler { mHandler = new Handler(looper); mNiHandler = niHandler; mConnMgr = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - mSuplConnectivityCallback = createSuplConnectivityCallback(); + mSuplConnectivityCallback = null; } /** @@ -584,11 +584,21 @@ class GnssNetworkConnectivityHandler { networkRequestBuilder.setNetworkSpecifier(Integer.toString(mActiveSubId)); } NetworkRequest networkRequest = networkRequestBuilder.build(); - mConnMgr.requestNetwork( - networkRequest, - mSuplConnectivityCallback, - mHandler, - SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS); + // Make sure we only have a single request. + if (mSuplConnectivityCallback != null) { + mConnMgr.unregisterNetworkCallback(mSuplConnectivityCallback); + } + mSuplConnectivityCallback = createSuplConnectivityCallback(); + try { + mConnMgr.requestNetwork( + networkRequest, + mSuplConnectivityCallback, + mHandler, + SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to request network.", e); + handleReleaseSuplConnection(GPS_AGPS_DATA_CONN_FAILED); + } } private int getNetworkCapability(int agpsType) { @@ -619,7 +629,10 @@ class GnssNetworkConnectivityHandler { } mAGpsDataConnectionState = AGPS_DATA_CONNECTION_CLOSED; - mConnMgr.unregisterNetworkCallback(mSuplConnectivityCallback); + if (mSuplConnectivityCallback != null) { + mConnMgr.unregisterNetworkCallback(mSuplConnectivityCallback); + mSuplConnectivityCallback = null; + } switch (agpsDataConnStatus) { case GPS_AGPS_DATA_CONN_FAILED: native_agps_data_conn_failed(); diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java index 21beb964529a..1bcc21e66302 100644 --- a/services/core/java/com/android/server/logcat/LogcatManagerService.java +++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java @@ -410,7 +410,8 @@ public final class LogcatManagerService extends SystemService { } private void processNewLogAccessRequest(LogAccessClient client) { - boolean isInstrumented = mActivityManagerInternal.isUidCurrentlyInstrumented(client.mUid); + boolean isInstrumented = mActivityManagerInternal.getInstrumentationSourceUid(client.mUid) + != android.os.Process.INVALID_UID; // The instrumented apks only run for testing, so we don't check user permission. if (isInstrumented) { diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9042326c5760..0eda980a29b6 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -656,7 +656,6 @@ public class NotificationManagerService extends SystemService { private int mWarnRemoteViewsSizeBytes; private int mStripRemoteViewsSizeBytes; - private boolean mForceUserSetOnUpgrade; private MetricsLogger mMetricsLogger; private NotificationChannelLogger mNotificationChannelLogger; @@ -2284,6 +2283,7 @@ public class NotificationManagerService extends SystemService { mNotificationChannelLogger, mAppOps, new SysUiStatsEvent.BuilderFactory()); + mPreferencesHelper.updateFixedImportance(mUm.getUsers()); mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper, @@ -2471,9 +2471,6 @@ public class NotificationManagerService extends SystemService { WorkerHandler handler = new WorkerHandler(Looper.myLooper()); - mForceUserSetOnUpgrade = getContext().getResources().getBoolean( - R.bool.config_notificationForceUserSetOnUpgrade); - init(handler, new RankingHandlerWorker(mRankingThread.getLooper()), AppGlobals.getPackageManager(), getContext().getPackageManager(), getLocalService(LightsManager.class), @@ -2502,8 +2499,7 @@ public class NotificationManagerService extends SystemService { LocalServices.getService(ActivityManagerInternal.class), createToastRateLimiter(), new PermissionHelper(LocalServices.getService( PermissionManagerServiceInternal.class), AppGlobals.getPackageManager(), - AppGlobals.getPermissionManager(), - mForceUserSetOnUpgrade), + AppGlobals.getPermissionManager()), LocalServices.getService(UsageStatsManagerInternal.class), getContext().getSystemService(TelecomManager.class), new NotificationChannelLoggerImpl()); @@ -3632,6 +3628,12 @@ public class NotificationManagerService extends SystemService { } @Override + public boolean isImportanceLocked(String pkg, int uid) { + checkCallerIsSystem(); + return mPreferencesHelper.isImportanceLocked(pkg, uid); + } + + @Override public boolean canShowBadge(String pkg, int uid) { checkCallerIsSystem(); return mPreferencesHelper.canShowBadge(pkg, uid); @@ -6146,7 +6148,6 @@ public class NotificationManagerService extends SystemService { pw.println(" mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate); pw.println(" hideSilentStatusBar=" + mPreferencesHelper.shouldHideSilentStatusIcons()); - pw.println(" mForceUserSetOnUpgrade=" + mForceUserSetOnUpgrade); } pw.println(" mArchive=" + mArchive.toString()); mArchive.dumpImpl(pw, filter); diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java index b2fee1e8b545..09ed56745e54 100644 --- a/services/core/java/com/android/server/notification/PermissionHelper.java +++ b/services/core/java/com/android/server/notification/PermissionHelper.java @@ -16,7 +16,6 @@ package com.android.server.notification; -import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET; import static android.content.pm.PackageManager.GET_PERMISSIONS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -55,14 +54,12 @@ public final class PermissionHelper { private final PermissionManagerServiceInternal mPmi; private final IPackageManager mPackageManager; private final IPermissionManager mPermManager; - private final boolean mForceUserSetOnUpgrade; public PermissionHelper(PermissionManagerServiceInternal pmi, IPackageManager packageManager, - IPermissionManager permManager, boolean forceUserSetOnUpgrade) { + IPermissionManager permManager) { mPmi = pmi; mPackageManager = packageManager; mPermManager = permManager; - mForceUserSetOnUpgrade = forceUserSetOnUpgrade; } /** @@ -72,8 +69,7 @@ public final class PermissionHelper { public boolean hasPermission(int uid) { final long callingId = Binder.clearCallingIdentity(); try { - return mPmi.checkPostNotificationsPermissionGrantedOrLegacyAccess(uid) - == PERMISSION_GRANTED; + return mPmi.checkUidPermission(uid, NOTIFICATION_PERMISSION) == PERMISSION_GRANTED; } finally { Binder.restoreCallingIdentity(callingId); } @@ -153,21 +149,13 @@ public final class PermissionHelper { } /** - * @see setNotificationPermission(String, int, boolean, boolean, boolean) - */ - public void setNotificationPermission(String packageName, @UserIdInt int userId, boolean grant, - boolean userSet) { - setNotificationPermission(packageName, userId, grant, userSet, false); - } - - /** * Grants or revokes the notification permission for a given package/user. UserSet should * only be true if this method is being called to migrate existing user choice, because it * can prevent the user from seeing the in app permission dialog. Must not be called * with a lock held. */ public void setNotificationPermission(String packageName, @UserIdInt int userId, boolean grant, - boolean userSet, boolean reviewRequired) { + boolean userSet) { final long callingId = Binder.clearCallingIdentity(); try { // Do not change the permission if the package doesn't request it, do not change fixed @@ -181,7 +169,7 @@ public final class PermissionHelper { boolean currentlyGranted = mPmi.checkPermission(packageName, NOTIFICATION_PERMISSION, userId) != PackageManager.PERMISSION_DENIED; - if (grant && !reviewRequired && !currentlyGranted) { + if (grant && !currentlyGranted) { mPermManager.grantRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId); } else if (!grant && currentlyGranted) { mPermManager.revokeRuntimePermission(packageName, NOTIFICATION_PERMISSION, @@ -189,12 +177,10 @@ public final class PermissionHelper { } if (userSet) { mPermManager.updatePermissionFlags(packageName, NOTIFICATION_PERMISSION, - FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED, - FLAG_PERMISSION_USER_SET, true, userId); - } else if (reviewRequired) { + FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, userId); + } else { mPermManager.updatePermissionFlags(packageName, NOTIFICATION_PERMISSION, - FLAG_PERMISSION_REVIEW_REQUIRED, FLAG_PERMISSION_REVIEW_REQUIRED, true, - userId); + 0, FLAG_PERMISSION_USER_SET, true, userId); } } catch (RemoteException e) { Slog.e(TAG, "Could not reach system server", e); @@ -212,9 +198,8 @@ public final class PermissionHelper { return; } if (!isPermissionFixed(pkgPerm.packageName, pkgPerm.userId)) { - boolean userSet = mForceUserSetOnUpgrade ? true : pkgPerm.userModifiedSettings; setNotificationPermission(pkgPerm.packageName, pkgPerm.userId, pkgPerm.granted, - userSet, !userSet); + true /* userSet always true on upgrade */); } } diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 4e3fbaa18870..97133a56779d 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -42,8 +42,10 @@ import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; +import android.content.pm.UserInfo; import android.metrics.LogMaker; import android.os.Binder; import android.os.Build; @@ -146,7 +148,6 @@ public class PreferencesHelper implements RankingConfig { static final boolean DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS = false; private static final boolean DEFAULT_SHOW_BADGE = true; - private static final boolean DEFAULT_OEM_LOCKED_IMPORTANCE = false; private static final boolean DEFAULT_APP_LOCKED_IMPORTANCE = false; static final boolean DEFAULT_BUBBLES_ENABLED = true; @@ -193,8 +194,6 @@ public class PreferencesHelper implements RankingConfig { private boolean mAllowInvalidShortcuts = false; - private Map<String, List<String>> mOemLockedApps = new HashMap(); - public PreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler, ZenModeHelper zenHelper, PermissionHelper permHelper, NotificationChannelLogger notificationChannelLogger, @@ -411,7 +410,7 @@ public class PreferencesHelper implements RankingConfig { channel.populateFromXml(parser); } channel.setImportanceLockedByCriticalDeviceFunction( - r.defaultAppLockedImportance); + r.defaultAppLockedImportance || r.fixedImportance); if (isShortcutOk(channel) && isDeletionOk(channel)) { r.channels.put(id, channel); @@ -484,14 +483,6 @@ public class PreferencesHelper implements RankingConfig { r.visibility = visibility; r.showBadge = showBadge; r.bubblePreference = bubblePreference; - if (mOemLockedApps.containsKey(r.pkg)) { - List<String> channels = mOemLockedApps.get(r.pkg); - if (channels == null || channels.isEmpty()) { - r.oemLockedImportance = true; - } else { - r.oemLockedChannels = channels; - } - } try { createDefaultChannelIfNeededLocked(r); @@ -818,6 +809,13 @@ public class PreferencesHelper implements RankingConfig { } } + boolean isImportanceLocked(String pkg, int uid) { + synchronized (mPackagePreferences) { + PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid); + return r.fixedImportance || r.defaultAppLockedImportance; + } + } + @Override public boolean isGroupBlocked(String packageName, int uid, String groupId) { if (groupId == null) { @@ -1008,7 +1006,7 @@ public class PreferencesHelper implements RankingConfig { clearLockedFieldsLocked(channel); channel.setImportanceLockedByCriticalDeviceFunction( - r.defaultAppLockedImportance); + r.defaultAppLockedImportance || r.fixedImportance); if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { channel.setLockscreenVisibility( @@ -1090,8 +1088,7 @@ public class PreferencesHelper implements RankingConfig { updatedChannel.unlockFields(updatedChannel.getUserLockedFields()); } - if ((mPermissionHelper.isPermissionFixed(r.pkg, UserHandle.getUserId(r.uid)) - || channel.isImportanceLockedByCriticalDeviceFunction()) + if (channel.isImportanceLockedByCriticalDeviceFunction() && !(channel.isBlockable() || channel.getImportance() == IMPORTANCE_NONE)) { updatedChannel.setImportance(channel.getImportance()); } @@ -1267,6 +1264,28 @@ public class PreferencesHelper implements RankingConfig { mHideSilentStatusBarIcons = hide; } + public void updateFixedImportance(List<UserInfo> users) { + for (UserInfo user : users) { + List<PackageInfo> packages = mPm.getInstalledPackagesAsUser( + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY), + user.getUserHandle().getIdentifier()); + for (PackageInfo pi : packages) { + boolean fixed = mPermissionHelper.isPermissionFixed( + pi.packageName, user.getUserHandle().getIdentifier()); + if (fixed) { + synchronized (mPackagePreferences) { + PackagePreferences p = getOrCreatePackagePreferencesLocked( + pi.packageName, pi.applicationInfo.uid); + p.fixedImportance = true; + for (NotificationChannel channel : p.channels.values()) { + channel.setImportanceLockedByCriticalDeviceFunction(true); + } + } + } + } + } + } + public void updateDefaultApps(int userId, ArraySet<String> toRemove, ArraySet<Pair<String, Integer>> toAdd) { synchronized (mPackagePreferences) { @@ -1274,8 +1293,10 @@ public class PreferencesHelper implements RankingConfig { if (userId == UserHandle.getUserId(p.uid)) { if (toRemove != null && toRemove.contains(p.pkg)) { p.defaultAppLockedImportance = false; - for (NotificationChannel channel : p.channels.values()) { - channel.setImportanceLockedByCriticalDeviceFunction(false); + if (!p.fixedImportance) { + for (NotificationChannel channel : p.channels.values()) { + channel.setImportanceLockedByCriticalDeviceFunction(false); + } } } } @@ -1934,13 +1955,9 @@ public class PreferencesHelper implements RankingConfig { pw.print(" defaultAppLocked="); pw.print(r.defaultAppLockedImportance); } - if (r.oemLockedImportance != DEFAULT_OEM_LOCKED_IMPORTANCE) { - pw.print(" oemLocked="); - pw.print(r.oemLockedImportance); - } - if (!r.oemLockedChannels.isEmpty()) { - pw.print(" futureLockedChannels="); - pw.print(r.oemLockedChannels); + if (r.fixedImportance != DEFAULT_APP_LOCKED_IMPORTANCE) { + pw.print(" fixedImportance="); + pw.print(r.fixedImportance); } pw.println(); for (NotificationChannel channel : r.channels.values()) { @@ -2682,9 +2699,8 @@ public class PreferencesHelper implements RankingConfig { int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; // these fields are loaded on boot from a different source of truth and so are not // written to notification policy xml - boolean oemLockedImportance = DEFAULT_OEM_LOCKED_IMPORTANCE; - List<String> oemLockedChannels = new ArrayList<>(); boolean defaultAppLockedImportance = DEFAULT_APP_LOCKED_IMPORTANCE; + boolean fixedImportance = DEFAULT_APP_LOCKED_IMPORTANCE; boolean hasSentInvalidMessage = false; boolean hasSentValidMessage = false; diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java index 7004c7363122..e7bcb0aa13c8 100644 --- a/services/core/java/com/android/server/pm/AppsFilterBase.java +++ b/services/core/java/com/android/server/pm/AppsFilterBase.java @@ -173,7 +173,7 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { * {@link #shouldFilterApplicationInternal(PackageDataSnapshot, int, Object, * PackageStateInternal, int)} call. * NOTE: It can only be relied upon after the system is ready to avoid unnecessary update on - * initial scam and is empty until {@link #mSystemReady} is true. + * initial scam and is empty until {@link #mCacheReady} is true. */ @NonNull @Watched @@ -181,7 +181,7 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { @NonNull protected SnapshotCache<WatchedSparseBooleanMatrix> mShouldFilterCacheSnapshot; - protected volatile boolean mSystemReady = false; + protected volatile boolean mCacheReady = false; protected boolean isForceQueryable(int callingAppId) { return mForceQueryable.contains(callingAppId); @@ -312,7 +312,7 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { || callingAppId == targetPkgSetting.getAppId()) { return false; } - if (mSystemReady) { // use cache + if (mCacheReady) { // use cache if (!shouldFilterApplicationUsingCache(callingUid, targetPkgSetting.getAppId(), userId)) { diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 952711db1735..c817e24d3b82 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -404,7 +404,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, + recipientUid + " -> " + visibleUid); } - if (mSystemReady) { + // TODO(b/231528435): invalidate cache instead of locking. + if (true/*mCacheReady*/) { synchronized (mCacheLock) { // update the cache in a one-off manner since we've got all the information we // need. @@ -420,7 +421,6 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, mFeatureConfig.onSystemReady(); updateEntireShouldFilterCacheAsync(pmInternal); - mSystemReady = true; } /** @@ -444,7 +444,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, final UserInfo[] users = snapshot.getUserInfos(); final ArraySet<String> additionalChangedPackages = addPackageInternal(newPkgSetting, settings); - if (mSystemReady) { + // TODO(b/231528435): invalidate cache instead of locking. + if (true/*mCacheReady*/) { synchronized (mCacheLock) { updateShouldFilterCacheForPackage(snapshot, null, newPkgSetting, settings, users, USER_ALL, settings.size()); @@ -586,7 +587,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } private void removeAppIdFromVisibilityCache(int appId) { - if (!mSystemReady) { + if (!mCacheReady) { return; } synchronized (mCacheLock) { @@ -661,18 +662,20 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, updateEntireShouldFilterCacheInner(snapshot, settings, usersRef[0], USER_ALL); onChanged(); + + mCacheReady = true; }); } public void onUserCreated(PackageDataSnapshot snapshot, int newUserId) { - if (!mSystemReady) { + if (!mCacheReady) { return; } updateEntireShouldFilterCache(snapshot, newUserId); } public void onUserDeleted(@UserIdInt int userId) { - if (!mSystemReady) { + if (!mCacheReady) { return; } removeShouldFilterCacheForUser(userId); @@ -681,7 +684,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, private void updateShouldFilterCacheForPackage(PackageDataSnapshot snapshot, String packageName) { - if (!mSystemReady) { + if (!mCacheReady) { return; } final ArrayMap<String, ? extends PackageStateInternal> settings = @@ -930,7 +933,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } removeAppIdFromVisibilityCache(setting.getAppId()); - if (mSystemReady && setting.hasSharedUser()) { + // TODO(b/231528435): invalidate cache instead of locking. + if (/*mCacheReady && */setting.hasSharedUser()) { final ArraySet<? extends PackageStateInternal> sharedUserPackages = getSharedUserPackages(setting.getSharedUserAppId(), sharedUserSettings); for (int i = sharedUserPackages.size() - 1; i >= 0; i--) { @@ -947,7 +951,8 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, } } - if (mSystemReady) { + // TODO(b/231528435): invalidate cache instead of locking. + if (true/*mCacheReady*/) { if (additionalChangedPackages != null) { for (int index = 0; index < additionalChangedPackages.size(); index++) { String changedPackage = additionalChangedPackages.valueAt(index); diff --git a/services/core/java/com/android/server/pm/AppsFilterLocked.java b/services/core/java/com/android/server/pm/AppsFilterLocked.java index e8e6cd9e8edb..b85bd8fbaa87 100644 --- a/services/core/java/com/android/server/pm/AppsFilterLocked.java +++ b/services/core/java/com/android/server/pm/AppsFilterLocked.java @@ -32,7 +32,7 @@ abstract class AppsFilterLocked extends AppsFilterBase { /** * Guards the access for {@link AppsFilterBase#mShouldFilterCache}; */ - protected Object mCacheLock = new Object(); + protected final Object mCacheLock = new Object(); @Override protected boolean isForceQueryable(int appId) { diff --git a/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java b/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java index c12aa6d485a4..39b63415e42e 100644 --- a/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java @@ -17,6 +17,7 @@ package com.android.server.pm; import com.android.server.utils.SnapshotCache; +import com.android.server.utils.WatchedSparseBooleanMatrix; import java.util.Arrays; @@ -49,12 +50,18 @@ public final class AppsFilterSnapshotImpl extends AppsFilterBase { mFeatureConfig = orig.mFeatureConfig.snapshot(); mOverlayReferenceMapper = orig.mOverlayReferenceMapper; mSystemSigningDetails = orig.mSystemSigningDetails; - synchronized (orig.mCacheLock) { - mShouldFilterCache = orig.mShouldFilterCacheSnapshot.snapshot(); - mShouldFilterCacheSnapshot = new SnapshotCache.Sealed<>(); + + mCacheReady = orig.mCacheReady; + if (mCacheReady) { + synchronized (orig.mCacheLock) { + mShouldFilterCache = orig.mShouldFilterCacheSnapshot.snapshot(); + } + } else { + // cache is not ready, use an empty cache for the snapshot + mShouldFilterCache = new WatchedSparseBooleanMatrix(); } + mShouldFilterCacheSnapshot = new SnapshotCache.Sealed<>(); mBackgroundExecutor = null; - mSystemReady = orig.mSystemReady; } } diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index d26a1ac4fba0..5a01ccbb7d6f 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -17,6 +17,7 @@ package com.android.server.pm; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; +import static com.android.server.pm.dex.ArtStatsLogUtils.BackgroundDexoptJobStatsLogger; import android.annotation.IntDef; import android.annotation.NonNull; @@ -77,43 +78,46 @@ public final class BackgroundDexOptService { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - @VisibleForTesting - static final int JOB_IDLE_OPTIMIZE = 800; - @VisibleForTesting - static final int JOB_POST_BOOT_UPDATE = 801; + @VisibleForTesting static final int JOB_IDLE_OPTIMIZE = 800; + @VisibleForTesting static final int JOB_POST_BOOT_UPDATE = 801; private static final long IDLE_OPTIMIZATION_PERIOD = TimeUnit.DAYS.toMillis(1); private static final long CANCELLATION_WAIT_CHECK_INTERVAL_MS = 200; - private static ComponentName sDexoptServiceName = new ComponentName("android", - BackgroundDexOptJobService.class.getName()); + private static ComponentName sDexoptServiceName = + new ComponentName("android", BackgroundDexOptJobService.class.getName()); // Possible return codes of individual optimization steps. /** Ok status: Optimizations finished, All packages were processed, can continue */ - private static final int STATUS_OK = 0; + public static final int STATUS_OK = 0; /** Optimizations should be aborted. Job scheduler requested it. */ - private static final int STATUS_ABORT_BY_CANCELLATION = 1; + public static final int STATUS_ABORT_BY_CANCELLATION = 1; /** Optimizations should be aborted. No space left on device. */ - private static final int STATUS_ABORT_NO_SPACE_LEFT = 2; + public static final int STATUS_ABORT_NO_SPACE_LEFT = 2; /** Optimizations should be aborted. Thermal throttling level too high. */ - private static final int STATUS_ABORT_THERMAL = 3; + public static final int STATUS_ABORT_THERMAL = 3; /** Battery level too low */ - private static final int STATUS_ABORT_BATTERY = 4; - /** {@link PackageDexOptimizer#DEX_OPT_FAILED} case */ - private static final int STATUS_DEX_OPT_FAILED = 5; - - @IntDef(prefix = {"STATUS_"}, value = { - STATUS_OK, - STATUS_ABORT_BY_CANCELLATION, - STATUS_ABORT_NO_SPACE_LEFT, - STATUS_ABORT_THERMAL, - STATUS_ABORT_BATTERY, - STATUS_DEX_OPT_FAILED, - }) + public static final int STATUS_ABORT_BATTERY = 4; + /** + * {@link PackageDexOptimizer#DEX_OPT_FAILED} case. This state means some packages have failed + * compilation during the job. Note that the failure will not be permanent as the next dexopt + * job will exclude those failed packages. + */ + public static final int STATUS_DEX_OPT_FAILED = 5; + + @IntDef(prefix = {"STATUS_"}, + value = + { + STATUS_OK, + STATUS_ABORT_BY_CANCELLATION, + STATUS_ABORT_NO_SPACE_LEFT, + STATUS_ABORT_THERMAL, + STATUS_ABORT_BATTERY, + STATUS_DEX_OPT_FAILED, + }) @Retention(RetentionPolicy.SOURCE) - private @interface Status { - } + public @interface Status {} // Used for calculating space threshold for downgrading unused apps. private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2; @@ -125,33 +129,30 @@ public final class BackgroundDexOptService { private final DexOptHelper mDexOptHelper; + private final BackgroundDexoptJobStatsLogger mStatsLogger = + new BackgroundDexoptJobStatsLogger(); + private final Object mLock = new Object(); // Thread currently running dexopt. This will be null if dexopt is not running. // The thread running dexopt make sure to set this into null when the pending dexopt is // completed. - @GuardedBy("mLock") - @Nullable - private Thread mDexOptThread; + @GuardedBy("mLock") @Nullable private Thread mDexOptThread; // Thread currently cancelling dexopt. This thread is in blocked wait state until // cancellation is done. Only this thread can change states for control. The other threads, if // need to wait for cancellation, should just wait without doing any control. - @GuardedBy("mLock") - @Nullable - private Thread mDexOptCancellingThread; + @GuardedBy("mLock") @Nullable private Thread mDexOptCancellingThread; // Tells whether post boot update is completed or not. - @GuardedBy("mLock") - private boolean mFinishedPostBootUpdate; + @GuardedBy("mLock") private boolean mFinishedPostBootUpdate; - @GuardedBy("mLock") - @Status private int mLastExecutionStatus = STATUS_OK; + @GuardedBy("mLock") @Status private int mLastExecutionStatus = STATUS_OK; - @GuardedBy("mLock") - private long mLastExecutionStartTimeMs; - @GuardedBy("mLock") - private long mLastExecutionDurationMs; + @GuardedBy("mLock") private long mLastExecutionStartTimeMs; + @GuardedBy("mLock") private long mLastExecutionDurationIncludingSleepMs; + @GuardedBy("mLock") private long mLastExecutionStartUptimeMs; + @GuardedBy("mLock") private long mLastExecutionDurationMs; // Keeps packages cancelled from PDO for last session. This is for debugging. @GuardedBy("mLock") @@ -177,8 +178,8 @@ public final class BackgroundDexOptService { void onPackagesUpdated(ArraySet<String> updatedPackages); } - public BackgroundDexOptService(Context context, DexManager dexManager, - PackageManagerService pm) { + public BackgroundDexOptService( + Context context, DexManager dexManager, PackageManagerService pm) { this(new Injector(context, dexManager, pm)); } @@ -230,6 +231,10 @@ public final class BackgroundDexOptService { writer.println(mLastExecutionStatus); writer.print("mLastExecutionStartTimeMs:"); writer.println(mLastExecutionStartTimeMs); + writer.print("mLastExecutionDurationIncludingSleepMs:"); + writer.println(mLastExecutionDurationIncludingSleepMs); + writer.print("mLastExecutionStartUptimeMs:"); + writer.println(mLastExecutionStartUptimeMs); writer.print("mLastExecutionDurationMs:"); writer.println(mLastExecutionDurationMs); writer.print("now:"); @@ -357,17 +362,20 @@ public final class BackgroundDexOptService { resetStatesForNewDexOptRunLocked(mInjector.createAndStartThread( "BackgroundDexOptService_" + (isPostBootUpdateJob ? "PostBoot" : "Idle"), () -> { - TimingsTraceAndSlog tr = new TimingsTraceAndSlog(TAG, - Trace.TRACE_TAG_PACKAGE_MANAGER); + TimingsTraceAndSlog tr = + new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_PACKAGE_MANAGER); tr.traceBegin("jobExecution"); boolean completed = false; try { - completed = runIdleOptimization(pm, pkgs, - params.getJobId() == JOB_POST_BOOT_UPDATE); + completed = runIdleOptimization( + pm, pkgs, params.getJobId() == JOB_POST_BOOT_UPDATE); } finally { // Those cleanup should be done always. tr.traceEnd(); - Slog.i(TAG, "dexopt finishing. jobid:" + params.getJobId() - + " completed:" + completed); + Slog.i(TAG, + "dexopt finishing. jobid:" + params.getJobId() + + " completed:" + completed); + + writeStatsLog(params); if (params.getJobId() == JOB_POST_BOOT_UPDATE) { if (completed) { @@ -451,7 +459,7 @@ public final class BackgroundDexOptService { if (mDexOptThread != Thread.currentThread()) { throw new IllegalStateException( "Only mDexOptThread can mark completion, mDexOptThread:" + mDexOptThread - + " current:" + Thread.currentThread()); + + " current:" + Thread.currentThread()); } mDexOptThread = null; // Other threads may be waiting for completion. @@ -481,11 +489,10 @@ public final class BackgroundDexOptService { private void scheduleAJob(int jobId) { JobScheduler js = mInjector.getJobScheduler(); - JobInfo.Builder builder = new JobInfo.Builder(jobId, sDexoptServiceName) - .setRequiresDeviceIdle(true); + JobInfo.Builder builder = + new JobInfo.Builder(jobId, sDexoptServiceName).setRequiresDeviceIdle(true); if (jobId == JOB_IDLE_OPTIMIZE) { - builder.setRequiresCharging(true) - .setPeriodic(IDLE_OPTIMIZATION_PERIOD); + builder.setRequiresCharging(true).setPeriodic(IDLE_OPTIMIZATION_PERIOD); } js.schedule(builder.build()); } @@ -525,30 +532,36 @@ public final class BackgroundDexOptService { } } - /** Returns true if completed */ - private boolean runIdleOptimization(PackageManagerService pm, List<String> pkgs, - boolean isPostBootUpdate) { + /** + * Returns whether we've successfully run the job. Note that it will return true even if some + * packages may have failed compiling. + */ + private boolean runIdleOptimization( + PackageManagerService pm, List<String> pkgs, boolean isPostBootUpdate) { synchronized (mLock) { mLastExecutionStartTimeMs = SystemClock.elapsedRealtime(); + mLastExecutionDurationIncludingSleepMs = -1; + mLastExecutionStartUptimeMs = SystemClock.uptimeMillis(); mLastExecutionDurationMs = -1; } long lowStorageThreshold = getLowStorageThreshold(); - int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold, - isPostBootUpdate); + int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold, isPostBootUpdate); logStatus(status); synchronized (mLock) { mLastExecutionStatus = status; - mLastExecutionDurationMs = SystemClock.elapsedRealtime() - mLastExecutionStartTimeMs; + mLastExecutionDurationIncludingSleepMs = + SystemClock.elapsedRealtime() - mLastExecutionStartTimeMs; + mLastExecutionDurationMs = SystemClock.uptimeMillis() - mLastExecutionStartUptimeMs; } - return status == STATUS_OK; + return status == STATUS_OK || status == STATUS_DEX_OPT_FAILED; } /** Gets the size of the directory. It uses recursion to go over all files. */ private long getDirectorySize(File f) { long size = 0; if (f.isDirectory()) { - for (File file: f.listFiles()) { + for (File file : f.listFiles()) { size += getDirectorySize(file); } } else { @@ -599,8 +612,8 @@ public final class BackgroundDexOptService { // Only downgrade apps when space is low on device. // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean // up disk before user hits the actual lowStorageThreshold. - long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE - * lowStorageThreshold; + long lowStorageThresholdForDowngrade = + LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE * lowStorageThreshold; boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade); if (DEBUG) { Slog.d(TAG, "Should Downgrade " + shouldDowngrade); @@ -620,19 +633,20 @@ public final class BackgroundDexOptService { // Should be aborted by the scheduler. return abortCode; } - @DexOptResult int downgradeResult = downgradePackage(snapshot, pm, pkg, + @DexOptResult + int downgradeResult = downgradePackage(snapshot, pm, pkg, /* isForPrimaryDex= */ true, isPostBootUpdate); if (downgradeResult == PackageDexOptimizer.DEX_OPT_PERFORMED) { updatedPackages.add(pkg); } - @Status int status = convertPackageDexOptimizerStatusToInternal( - downgradeResult); + @Status + int status = convertPackageDexOptimizerStatusToInternal(downgradeResult); if (status != STATUS_OK) { return status; } if (supportSecondaryDex) { downgradeResult = downgradePackage(snapshot, pm, pkg, - /* isForPrimaryDex= */false, isPostBootUpdate); + /* isForPrimaryDex= */ false, isPostBootUpdate); status = convertPackageDexOptimizerStatusToInternal(downgradeResult); if (status != STATUS_OK) { return status; @@ -661,6 +675,11 @@ public final class BackgroundDexOptService { ArraySet<String> updatedPackages, boolean isPostBootUpdate) { boolean supportSecondaryDex = mInjector.supportSecondaryDex(); + // Keep the error if there is any error from any package. + @Status int status = STATUS_OK; + + // Other than cancellation, all packages will be processed even if an error happens + // in a package. for (String pkg : pkgs) { int abortCode = abortIdleOptimizations(lowStorageThreshold); if (abortCode != STATUS_OK) { @@ -668,26 +687,32 @@ public final class BackgroundDexOptService { return abortCode; } - @DexOptResult int primaryResult = - optimizePackage(pkg, true /* isForPrimaryDex */, isPostBootUpdate); + @DexOptResult + int primaryResult = optimizePackage(pkg, true /* isForPrimaryDex */, isPostBootUpdate); + if (primaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) { + return STATUS_ABORT_BY_CANCELLATION; + } if (primaryResult == PackageDexOptimizer.DEX_OPT_PERFORMED) { updatedPackages.add(pkg); - } else if (primaryResult != PackageDexOptimizer.DEX_OPT_SKIPPED) { - return convertPackageDexOptimizerStatusToInternal(primaryResult); + } else if (primaryResult == PackageDexOptimizer.DEX_OPT_FAILED) { + status = convertPackageDexOptimizerStatusToInternal(primaryResult); } if (!supportSecondaryDex) { continue; } - @DexOptResult int secondaryResult = + @DexOptResult + int secondaryResult = optimizePackage(pkg, false /* isForPrimaryDex */, isPostBootUpdate); - if (secondaryResult != PackageDexOptimizer.DEX_OPT_PERFORMED - && secondaryResult != PackageDexOptimizer.DEX_OPT_SKIPPED) { - return convertPackageDexOptimizerStatusToInternal(secondaryResult); + if (secondaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) { + return STATUS_ABORT_BY_CANCELLATION; + } + if (secondaryResult == PackageDexOptimizer.DEX_OPT_FAILED) { + status = convertPackageDexOptimizerStatusToInternal(secondaryResult); } } - return STATUS_OK; + return status; } /** @@ -731,7 +756,7 @@ public final class BackgroundDexOptService { if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) { final Computer newSnapshot = pm.snapshotComputer(); FrameworkStatsLog.write(FrameworkStatsLog.APP_DOWNGRADED, pkg, package_size_before, - getPackageSize(newSnapshot, pkg), /*aggressive=*/ false); + getPackageSize(newSnapshot, pkg), /*aggressive=*/false); } return result; } @@ -760,7 +785,7 @@ public final class BackgroundDexOptService { @DexOptResult private int optimizePackage(String pkg, boolean isForPrimaryDex, boolean isPostBootUpdate) { int reason = isPostBootUpdate ? PackageManagerService.REASON_POST_BOOT - : PackageManagerService.REASON_BACKGROUND_DEXOPT; + : PackageManagerService.REASON_BACKGROUND_DEXOPT; int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE; if (!isPostBootUpdate) { dexoptFlags |= DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES @@ -777,22 +802,21 @@ public final class BackgroundDexOptService { } @DexOptResult - private int performDexOptPrimary(String pkg, int reason, - int dexoptFlags) { - return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true, - () -> mDexOptHelper.performDexOptWithStatus( - new DexoptOptions(pkg, reason, dexoptFlags))); + private int performDexOptPrimary(String pkg, int reason, int dexoptFlags) { + DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, dexoptFlags); + return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/true, + () -> mDexOptHelper.performDexOptWithStatus(dexoptOptions)); } @DexOptResult - private int performDexOptSecondary(String pkg, int reason, - int dexoptFlags) { - DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, - dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX); - return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false, - () -> mDexOptHelper.performDexOpt(dexoptOptions) - ? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED - ); + private int performDexOptSecondary(String pkg, int reason, int dexoptFlags) { + DexoptOptions dexoptOptions = new DexoptOptions( + pkg, reason, dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX); + return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/false, + () + -> mDexOptHelper.performDexOpt(dexoptOptions) + ? PackageDexOptimizer.DEX_OPT_PERFORMED + : PackageDexOptimizer.DEX_OPT_FAILED); } /** @@ -805,8 +829,8 @@ public final class BackgroundDexOptService { * {@link PackageDexOptimizer#DEX_OPT_FAILED} */ @DexOptResult - private int trackPerformDexOpt(String pkg, boolean isForPrimaryDex, - Supplier<Integer> performDexOptWrapper) { + private int trackPerformDexOpt( + String pkg, boolean isForPrimaryDex, Supplier<Integer> performDexOptWrapper) { ArraySet<String> failedPackageNames; synchronized (mLock) { failedPackageNames = @@ -923,6 +947,19 @@ public final class BackgroundDexOptService { } } + private void writeStatsLog(JobParameters params) { + @Status int status; + long durationMs; + long durationIncludingSleepMs; + synchronized (mLock) { + status = mLastExecutionStatus; + durationMs = mLastExecutionDurationMs; + durationIncludingSleepMs = mLastExecutionDurationIncludingSleepMs; + } + + mStatsLogger.write(status, params.getStopReason(), durationMs, durationIncludingSleepMs); + } + /** Injector pattern for testing purpose */ @VisibleForTesting static final class Injector { @@ -962,8 +999,8 @@ public final class BackgroundDexOptService { } boolean isBackgroundDexOptDisabled() { - return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */, - false /* default */); + return SystemProperties.getBoolean( + "pm.dexopt.disable_bg_dexopt" /* key */, false /* default */); } boolean isBatteryLevelLow() { @@ -993,8 +1030,8 @@ public final class BackgroundDexOptService { } int getCurrentThermalStatus() { - IThermalService thermalService = IThermalService.Stub - .asInterface(ServiceManager.getService(Context.THERMAL_SERVICE)); + IThermalService thermalService = IThermalService.Stub.asInterface( + ServiceManager.getService(Context.THERMAL_SERVICE)); try { return thermalService.getCurrentThermalStatus(); } catch (RemoteException e) { @@ -1003,8 +1040,8 @@ public final class BackgroundDexOptService { } int getDexOptThermalCutoff() { - return SystemProperties.getInt("dalvik.vm.dexopt.thermal-cutoff", - THERMAL_CUTOFF_DEFAULT); + return SystemProperties.getInt( + "dalvik.vm.dexopt.thermal-cutoff", THERMAL_CUTOFF_DEFAULT); } Thread createAndStartThread(String name, Runnable target) { diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java index ed71f1eb5313..9d1f0704a3cf 100644 --- a/services/core/java/com/android/server/pm/BroadcastHelper.java +++ b/services/core/java/com/android/server/pm/BroadcastHelper.java @@ -197,7 +197,7 @@ public final class BroadcastHelper { final BroadcastOptions bOptions = getTemporaryAppAllowlistBroadcastOptions( REASON_LOCKED_BOOT_COMPLETED); am.broadcastIntentWithFeature(null, null, lockedBcIntent, null, null, 0, null, null, - requiredPermissions, null, android.app.AppOpsManager.OP_NONE, + requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, bOptions.toBundle(), false, false, userId); // Deliver BOOT_COMPLETED only if user is unlocked @@ -207,7 +207,7 @@ public final class BroadcastHelper { bcIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); } am.broadcastIntentWithFeature(null, null, bcIntent, null, null, 0, null, null, - requiredPermissions, null, android.app.AppOpsManager.OP_NONE, + requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, bOptions.toBundle(), false, false, userId); } } catch (RemoteException e) { @@ -263,7 +263,7 @@ public final class BroadcastHelper { }; try { am.broadcastIntentWithFeature(null, null, intent, null, null, 0, null, null, - requiredPermissions, null, android.app.AppOpsManager.OP_NONE, null, false, + requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, null, false, false, UserHandle.USER_ALL); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -301,7 +301,7 @@ public final class BroadcastHelper { intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); try { am.broadcastIntentWithFeature(null, null, intent, null, null, - 0, null, null, null, null, android.app.AppOpsManager.OP_NONE, + 0, null, null, null, null, null, android.app.AppOpsManager.OP_NONE, null, false, false, userId); } catch (RemoteException e) { } diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index db48a1f63099..eb635500580a 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -94,12 +94,13 @@ import java.util.Set; @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) public interface Computer extends PackageDataSnapshot { + int getVersion(); + /** * Administrative statistics: record that the snapshot has been used. Every call * to use() increments the usage counter. */ - default void use() { - } + Computer use(); /** * Fetch the snapshot usage counter. * @return The number of times this snapshot was used. diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index bf9f4fa8a211..f3c41af05da3 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -367,6 +367,8 @@ public class ComputerEngine implements Computer { return (v1 > v2) ? -1 : ((v1 < v2) ? 1 : 0); }; + private final int mVersion; + // The administrative use counter. private int mUsed = 0; @@ -424,7 +426,8 @@ public class ComputerEngine implements Computer { return mLocalAndroidApplication; } - ComputerEngine(PackageManagerService.Snapshot args) { + ComputerEngine(PackageManagerService.Snapshot args, int version) { + mVersion = version; mSettings = new Settings(args.settings); mIsolatedOwners = args.isolatedOwners; mPackages = args.packages; @@ -464,11 +467,17 @@ public class ComputerEngine implements Computer { mService = args.service; } + @Override + public int getVersion() { + return mVersion; + } + /** * Record that the snapshot was used. */ - public final void use() { + public final Computer use() { mUsed++; + return this; } /** diff --git a/services/core/java/com/android/server/pm/ComputerLocked.java b/services/core/java/com/android/server/pm/ComputerLocked.java index af196d51331f..37070dbe7131 100644 --- a/services/core/java/com/android/server/pm/ComputerLocked.java +++ b/services/core/java/com/android/server/pm/ComputerLocked.java @@ -30,7 +30,7 @@ import com.android.internal.annotations.VisibleForTesting; public final class ComputerLocked extends ComputerEngine { ComputerLocked(PackageManagerService.Snapshot args) { - super(args); + super(args, -1); } protected ComponentName resolveComponentName() { diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index cb38d522f3bd..0cdf7bf62f55 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -166,9 +166,10 @@ final class DeletePackageHelper { if (PackageManagerServiceUtils.isSystemApp(uninstalledPs)) { UserInfo userInfo = mUserManagerInternal.getUserInfo(userId); - if (userInfo == null || !userInfo.isAdmin()) { + if (userInfo == null || (!userInfo.isAdmin() && !mUserManagerInternal.getUserInfo( + mUserManagerInternal.getProfileParentId(userId)).isAdmin())) { Slog.w(TAG, "Not removing package " + packageName - + " as only admin user may downgrade system apps"); + + " as only admin user (or their profile) may downgrade system apps"); EventLog.writeEvent(0x534e4554, "170646036", -1, packageName); return PackageManager.DELETE_FAILED_USER_RESTRICTED; } diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 8c33dd935822..29b122d56ac5 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -26,10 +26,13 @@ import static com.android.server.pm.PackageManagerService.REASON_CMDLINE; import static com.android.server.pm.PackageManagerService.REASON_FIRST_BOOT; import static com.android.server.pm.PackageManagerService.STUB_SUFFIX; import static com.android.server.pm.PackageManagerService.TAG; +import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason; import static com.android.server.pm.PackageManagerServiceCompilerMapping.getDefaultCompilerFilter; import static com.android.server.pm.PackageManagerServiceUtils.REMOVE_IF_NULL_PKG; +import android.Manifest; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.AppGlobals; import android.content.Intent; @@ -42,6 +45,7 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.Log; import android.util.Slog; @@ -55,6 +59,8 @@ import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.pm.parsing.pkg.AndroidPackageUtils; import com.android.server.pm.pkg.PackageStateInternal; +import dalvik.system.DexFile; + import java.io.File; import java.util.ArrayList; import java.util.Collection; @@ -250,10 +256,71 @@ final class DexOptHelper { numberOfPackagesFailed}; } + /** + * Checks if system UI package (typically "com.android.systemui") needs to be re-compiled, and + * compiles it if needed. + */ + private void checkAndDexOptSystemUi() { + Computer snapshot = mPm.snapshotComputer(); + String sysUiPackageName = + mPm.mContext.getString(com.android.internal.R.string.config_systemUi); + AndroidPackage pkg = snapshot.getPackage(sysUiPackageName); + if (pkg == null) { + Log.w(TAG, "System UI package " + sysUiPackageName + " is not found for dexopting"); + return; + } + + // It could also be after mainline update, but we're not introducing a new reason just for + // this special case. + int reason = REASON_BOOT_AFTER_OTA; + + String defaultCompilerFilter = getCompilerFilterForReason(reason); + String targetCompilerFilter = + SystemProperties.get("dalvik.vm.systemuicompilerfilter", defaultCompilerFilter); + String compilerFilter; + + if (DexFile.isProfileGuidedCompilerFilter(targetCompilerFilter)) { + compilerFilter = defaultCompilerFilter; + File profileFile = new File(getPrebuildProfilePath(pkg)); + + // Copy the profile to the reference profile path if it exists. Installd can only use a + // profile at the reference profile path for dexopt. + if (profileFile.exists()) { + try { + synchronized (mPm.mInstallLock) { + if (mPm.mInstaller.copySystemProfile(profileFile.getAbsolutePath(), + pkg.getUid(), pkg.getPackageName(), + ArtManager.getProfileName(null))) { + compilerFilter = targetCompilerFilter; + } else { + Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath()); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath(), e); + } + } + } else { + compilerFilter = targetCompilerFilter; + } + + performDexOptTraced(new DexoptOptions(pkg.getPackageName(), REASON_BOOT_AFTER_OTA, + compilerFilter, null /* splitName */, 0 /* dexoptFlags */)); + } + + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public void performPackageDexOptUpgradeIfNeeded() { PackageManagerServiceUtils.enforceSystemOrRoot( "Only the system can request package update"); + // The default is "true". + if (!"false".equals(DeviceConfig.getProperty("runtime", "dexopt_system_ui_on_boot"))) { + // System UI is important to user experience, so we check it on every boot. It may need + // to be re-compiled after a mainline update or an OTA. + // TODO(b/227310505): Only do this after a mainline update or an OTA. + checkAndDexOptSystemUi(); + } + // We need to re-extract after an OTA. boolean causeUpgrade = mPm.isDeviceUpgrading(); diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index 32f0f109821d..a078b5d4e8b1 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -44,6 +44,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; public class Installer extends SystemService { private static final String TAG = "Installer"; @@ -118,9 +120,13 @@ public class Installer extends SystemService { public static final int FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES = IInstalld.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES; + private static final long CONNECT_RETRY_DELAY_MS = DateUtils.SECOND_IN_MILLIS; + private static final long CONNECT_WAIT_MS = 10 * DateUtils.SECOND_IN_MILLIS; + private final boolean mIsolated; private volatile boolean mDeferSetFirstBoot; - private volatile IInstalld mInstalld; + private volatile IInstalld mInstalld = null; + private volatile CountDownLatch mInstalldLatch = new CountDownLatch(1); private volatile Object mWarnIfHeld; public Installer(Context context) { @@ -149,6 +155,7 @@ public class Installer extends SystemService { public void onStart() { if (mIsolated) { mInstalld = null; + mInstalldLatch.countDown(); } else { connect(); } @@ -160,6 +167,7 @@ public class Installer extends SystemService { try { binder.linkToDeath(() -> { Slog.w(TAG, "installd died; reconnecting"); + mInstalldLatch = new CountDownLatch(1); connect(); }, 0); } catch (RemoteException e) { @@ -168,7 +176,9 @@ public class Installer extends SystemService { } if (binder != null) { - mInstalld = IInstalld.Stub.asInterface(binder); + IInstalld installd = IInstalld.Stub.asInterface(binder); + mInstalld = installd; + mInstalldLatch.countDown(); try { invalidateMounts(); executeDeferredActions(); @@ -176,7 +186,7 @@ public class Installer extends SystemService { } } else { Slog.w(TAG, "installd not found; trying again"); - BackgroundThread.getHandler().postDelayed(this::connect, DateUtils.SECOND_IN_MILLIS); + BackgroundThread.getHandler().postDelayed(this::connect, CONNECT_RETRY_DELAY_MS); } } @@ -194,7 +204,7 @@ public class Installer extends SystemService { * * @return if the remote call should continue. */ - private boolean checkBeforeRemote() { + private boolean checkBeforeRemote() throws InstallerException { if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) { Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x" + Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable()); @@ -202,9 +212,17 @@ public class Installer extends SystemService { if (mIsolated) { Slog.i(TAG, "Ignoring request because this installer is isolated"); return false; - } else { - return true; } + + try { + if (!mInstalldLatch.await(CONNECT_WAIT_MS, TimeUnit.MILLISECONDS)) { + throw new InstallerException("time out waiting for the installer to be ready"); + } + } catch (InterruptedException e) { + // Do nothing. + } + + return true; } // We explicitly do NOT set previousAppId because the default value should always be 0. diff --git a/services/core/java/com/android/server/pm/IntentResolverInterceptor.java b/services/core/java/com/android/server/pm/IntentResolverInterceptor.java deleted file mode 100644 index f5eee5ac160d..000000000000 --- a/services/core/java/com/android/server/pm/IntentResolverInterceptor.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.server.pm; - -import static com.android.server.wm.ActivityInterceptorCallback.INTENT_RESOLVER_ORDERED_ID; - -import android.Manifest; -import android.annotation.Nullable; -import android.annotation.RequiresPermission; -import android.content.ComponentName; -import android.content.Context; -import android.content.res.Resources; -import android.provider.DeviceConfig; -import android.util.Slog; - -import com.android.internal.R; -import com.android.internal.app.ChooserActivity; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.server.LocalServices; -import com.android.server.wm.ActivityInterceptorCallback; -import com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo; -import com.android.server.wm.ActivityTaskManagerInternal; - -/** - * Redirects Activity starts for the system bundled {@link ChooserActivity} to an external - * Sharesheet implementation by modifying the target component when appropriate. - * <p> - * Note: config_chooserActivity (Used also by ActivityTaskSupervisor) is already updated to point - * to the new instance. This value is read and used for the new target component. - */ -public final class IntentResolverInterceptor { - private static final String TAG = "IntentResolverIntercept"; - private final Context mContext; - private final ComponentName mFrameworkChooserComponent; - private final ComponentName mUnbundledChooserComponent; - private boolean mUseUnbundledSharesheet; - - private final ActivityInterceptorCallback mActivityInterceptorCallback = - new ActivityInterceptorCallback() { - @Nullable - @Override - public ActivityInterceptResult intercept(ActivityInterceptorInfo info) { - if (mUseUnbundledSharesheet && isSystemChooserActivity(info)) { - Slog.d(TAG, "Redirecting to UNBUNDLED Sharesheet"); - info.intent.setComponent(mUnbundledChooserComponent); - return new ActivityInterceptResult(info.intent, info.checkedOptions); - } - return null; - } - }; - - public IntentResolverInterceptor(Context context) { - mContext = context; - mFrameworkChooserComponent = new ComponentName(mContext, ChooserActivity.class); - mUnbundledChooserComponent = ComponentName.unflattenFromString( - Resources.getSystem().getString(R.string.config_chooserActivity)); - } - - /** - * Start listening for intents and USE_UNBUNDLED_SHARESHEET property changes. - */ - @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) - public void registerListeners() { - LocalServices.getService(ActivityTaskManagerInternal.class) - .registerActivityStartInterceptor(INTENT_RESOLVER_ORDERED_ID, - mActivityInterceptorCallback); - - DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, - mContext.getMainExecutor(), properties -> updateUseUnbundledSharesheet()); - updateUseUnbundledSharesheet(); - } - - @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) - private void updateUseUnbundledSharesheet() { - mUseUnbundledSharesheet = DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.USE_UNBUNDLED_SHARESHEET, - false); - if (mUseUnbundledSharesheet) { - Slog.d(TAG, "using UNBUNDLED Sharesheet"); - } else { - Slog.d(TAG, "using FRAMEWORK Sharesheet"); - } - } - - private boolean isSystemChooserActivity(ActivityInterceptorInfo info) { - return mFrameworkChooserComponent.getPackageName().equals(info.aInfo.packageName) - && mFrameworkChooserComponent.getClassName().equals(info.aInfo.name); - } -} diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java index ec6443db9f0f..652847ad1647 100644 --- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java +++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java @@ -315,9 +315,9 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { @Deprecated public final List<ResolveInfo> queryIntentReceivers(Intent intent, String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, - int filterCallingUid, int userId) { - return getResolveIntentHelper().queryIntentReceiversInternal( - snapshot(), intent, resolvedType, flags, userId, filterCallingUid); + int filterCallingUid, int userId, boolean forSend) { + return getResolveIntentHelper().queryIntentReceiversInternal(snapshot(), intent, + resolvedType, flags, userId, filterCallingUid, forSend); } @Override @@ -352,10 +352,9 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { @Override @Deprecated - public final void setDeviceOwnerProtectedPackages( - String deviceOwnerPackageName, List<String> packageNames) { - getProtectedPackages().setDeviceOwnerProtectedPackages( - deviceOwnerPackageName, packageNames); + public final void setOwnerProtectedPackages( + @UserIdInt int userId, @NonNull List<String> packageNames) { + getProtectedPackages().setOwnerProtectedPackages(userId, packageNames); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2cef35fcf0f6..386c4372ca65 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -271,8 +271,8 @@ import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** @@ -939,7 +939,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService private final DexOptHelper mDexOptHelper; private final SuspendPackageHelper mSuspendPackageHelper; private final DistractingPackageHelper mDistractingPackageHelper; - private final IntentResolverInterceptor mIntentResolverInterceptor; private final StorageEventHelper mStorageEventHelper; /** @@ -1038,22 +1037,15 @@ public class PackageManagerService implements PackageSender, TestUtilityService // times during the PackageManagerService constructor but it should not be modified thereafter. private ComputerLocked mLiveComputer; - // A lock-free cache for frequently called functions. - private volatile Computer mSnapshotComputer; + private static final AtomicReference<Computer> sSnapshot = new AtomicReference<>(); - // If true, the snapshot is invalid (stale). The attribute is static since it may be - // set from outside classes. The attribute may be set to true anywhere, although it - // should only be set true while holding mLock. However, the attribute id guaranteed - // to be set false only while mLock and mSnapshotLock are both held. - private static final AtomicBoolean sSnapshotInvalid = new AtomicBoolean(true); - - static final ThreadLocal<ThreadComputer> sThreadComputer = - ThreadLocal.withInitial(ThreadComputer::new); + // If this differs from Computer#getVersion, the snapshot is invalid (stale). + private static final AtomicInteger sSnapshotPendingVersion = new AtomicInteger(1); /** - * This lock is used to make reads from {@link #sSnapshotInvalid} and - * {@link #mSnapshotComputer} atomic inside {@code snapshotComputer()}. This lock is - * not meant to be used outside that method. This lock must be taken before + * This lock is used to make reads from {@link #sSnapshotPendingVersion} and + * {@link #sSnapshot} atomic inside {@code snapshotComputer()} when the versions mismatch. + * This lock is not meant to be used outside that method. This lock must be taken before * {@link #mLock} is taken. */ private final Object mSnapshotLock = new Object(); @@ -1077,48 +1069,53 @@ public class PackageManagerService implements PackageSender, TestUtilityService // yet invalidated the snapshot. Always give the thread the live computer. return mLiveComputer; } + + var oldSnapshot = sSnapshot.get(); + var pendingVersion = sSnapshotPendingVersion.get(); + + if (oldSnapshot != null && oldSnapshot.getVersion() == pendingVersion) { + return oldSnapshot.use(); + } + synchronized (mSnapshotLock) { - // This synchronization block serializes access to the snapshot computer and - // to the code that samples mSnapshotInvalid. - Computer c = mSnapshotComputer; - if (sSnapshotInvalid.getAndSet(false) || (c == null)) { - // The snapshot is invalid if it is marked as invalid or if it is null. If it - // is null, then it is currently being rebuilt by rebuildSnapshot(). - synchronized (mLock) { - // Rebuild the snapshot if it is invalid. Note that the snapshot might be - // invalidated as it is rebuilt. However, the snapshot is still - // self-consistent (the lock is being held) and is current as of the time - // this function is entered. - rebuildSnapshot(); - - // Guaranteed to be non-null. mSnapshotComputer is only be set to null - // temporarily in rebuildSnapshot(), which is guarded by mLock(). Since - // the mLock is held in this block and since rebuildSnapshot() is - // complete, the attribute can not now be null. - c = mSnapshotComputer; - } + // Re-capture pending version in case a new invalidation occurred since last check + var rebuildSnapshot = sSnapshot.get(); + var rebuildVersion = sSnapshotPendingVersion.get(); + + // Check the versions again while the lock is held, in case the rebuild time caused + // multiple threads to wait on the snapshot lock. When the first thread finishes + // a rebuild, the snapshot is now valid and the other waiting threads can use it + // without kicking off their own rebuilds. + if (rebuildSnapshot != null && rebuildSnapshot.getVersion() == rebuildVersion) { + return rebuildSnapshot.use(); + } + + synchronized (mLock) { + // Fetch version one last time to ensure that the rebuilt snapshot matches + // the latest invalidation, which could have come in between entering the + // SnapshotLock and mLock sync blocks. + rebuildVersion = sSnapshotPendingVersion.get(); + + // Build the snapshot for this version + var newSnapshot = rebuildSnapshot(rebuildSnapshot, rebuildVersion); + sSnapshot.set(newSnapshot); + return newSnapshot.use(); } - c.use(); - return c; } } - /** - * Rebuild the cached computer. mSnapshotComputer is temporarily set to null to block other - * threads from using the invalid computer until it is rebuilt. - */ @GuardedBy({ "mLock", "mSnapshotLock"}) - private void rebuildSnapshot() { - final long now = SystemClock.currentTimeMicro(); - final int hits = mSnapshotComputer == null ? -1 : mSnapshotComputer.getUsed(); - mSnapshotComputer = null; - final Snapshot args = new Snapshot(Snapshot.SNAPPED); - mSnapshotComputer = new ComputerEngine(args); - final long done = SystemClock.currentTimeMicro(); + private Computer rebuildSnapshot(@Nullable Computer oldSnapshot, int newVersion) { + var now = SystemClock.currentTimeMicro(); + var hits = oldSnapshot == null ? -1 : oldSnapshot.getUsed(); + var args = new Snapshot(Snapshot.SNAPPED); + var newSnapshot = new ComputerEngine(args, newVersion); + var done = SystemClock.currentTimeMicro(); if (mSnapshotStatistics != null) { mSnapshotStatistics.rebuild(now, done, hits); } + return newSnapshot; } /** @@ -1138,7 +1135,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService if (TRACE_SNAPSHOTS) { Log.i(TAG, "snapshot: onChange(" + what + ")"); } - sSnapshotInvalid.set(true); + sSnapshotPendingVersion.incrementAndGet(); } /** @@ -1665,7 +1662,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService mRequiredSdkSandboxPackage = testParams.requiredSdkSandboxPackage; mLiveComputer = createLiveComputer(); - mSnapshotComputer = null; mSnapshotStatistics = null; mPackages.putAll(testParams.packages); @@ -1691,7 +1687,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService mSharedLibraries.setDeletePackageHelper(mDeletePackageHelper); - mIntentResolverInterceptor = null; mStorageEventHelper = testParams.storageEventHelper; registerObservers(false); @@ -1855,9 +1850,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService // cached computer is the same as the live computer until the end of the // constructor, at which time the invalidation method updates it. mSnapshotStatistics = new SnapshotStatistics(); - sSnapshotInvalid.set(true); + sSnapshotPendingVersion.incrementAndGet(); mLiveComputer = createLiveComputer(); - mSnapshotComputer = null; registerObservers(true); } @@ -2252,8 +2246,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService mServiceStartWithDelay = SystemClock.uptimeMillis() + (60 * 1000L); - mIntentResolverInterceptor = new IntentResolverInterceptor(mContext); - Slog.i(TAG, "Fix for b/169414761 is applied"); } @@ -4143,11 +4135,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService // Prune unused static shared libraries which have been cached a period of time schedulePruneUnusedStaticSharedLibraries(false /* delay */); - - // TODO(b/222706900): Remove this intent interceptor before T launch - if (mIntentResolverInterceptor != null) { - mIntentResolverInterceptor.registerListeners(); - } } //TODO: b/111402650 @@ -5388,10 +5375,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService @Override public void setApplicationCategoryHint(String packageName, int categoryHint, String callerPackageName) { - final PackageStateMutator.InitialState initialState = recordInitialState(); - - final FunctionalUtils.ThrowingFunction<Computer, PackageStateMutator.Result> - implementation = computer -> { + final FunctionalUtils.ThrowingBiFunction<PackageStateMutator.InitialState, Computer, + PackageStateMutator.Result> implementation = (initialState, computer) -> { if (computer.getInstantAppPackageName(Binder.getCallingUid()) != null) { throw new SecurityException( "Instant applications don't have access to this method"); @@ -5419,12 +5404,13 @@ public class PackageManagerService implements PackageSender, TestUtilityService } }; - PackageStateMutator.Result result = implementation.apply(snapshotComputer()); + PackageStateMutator.Result result = + implementation.apply(recordInitialState(), snapshotComputer()); if (result != null && result.isStateChanged() && !result.isSpecificPackageNull()) { // TODO: Specific return value of what state changed? // The installer on record might have changed, retry with lock synchronized (mPackageStateWriteLock) { - result = implementation.apply(snapshotComputer()); + result = implementation.apply(recordInitialState(), snapshotComputer()); } } @@ -7156,9 +7142,19 @@ public class PackageManagerService implements PackageSender, TestUtilityService public PackageStateMutator.Result commitPackageStateMutation( @Nullable PackageStateMutator.InitialState initialState, @NonNull String packageName, @NonNull Consumer<PackageStateWrite> consumer) { + PackageStateMutator.Result result = null; + if (Thread.holdsLock(mPackageStateWriteLock)) { + // If the thread is already holding the lock, this is likely a retry based on a prior + // failure, and re-calculating whether a state change occurred can be skipped. + result = PackageStateMutator.Result.SUCCESS; + } synchronized (mPackageStateWriteLock) { - final PackageStateMutator.Result result = mPackageStateMutator.generateResult( - initialState, mChangedPackagesTracker.getSequenceNumber()); + if (result == null) { + // If the thread wasn't previously holding, this is a first-try commit and so a + // state change may have happened. + result = mPackageStateMutator.generateResult( + initialState, mChangedPackagesTracker.getSequenceNumber()); + } if (result != PackageStateMutator.Result.SUCCESS) { return result; } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 6b10d4c17527..1eb74facb840 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -2269,10 +2269,23 @@ class PackageManagerShellCommand extends ShellCommand { } private int runClear() throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); int userId = UserHandle.USER_SYSTEM; - String option = getNextOption(); - if (option != null && option.equals("--user")) { - userId = UserHandle.parseUserArg(getNextArgRequired()); + boolean cacheOnly = false; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + case "--cache-only": + cacheOnly = true; + break; + default: + pw.println("Error: Unknown option: " + opt); + return 1; + } } String pkg = getNextArg(); @@ -2284,7 +2297,12 @@ class PackageManagerShellCommand extends ShellCommand { final int translatedUserId = translateUserId(userId, UserHandle.USER_NULL, "runClear"); final ClearDataObserver obs = new ClearDataObserver(); - ActivityManager.getService().clearApplicationUserData(pkg, false, obs, translatedUserId); + if (!cacheOnly) { + ActivityManager.getService() + .clearApplicationUserData(pkg, false, obs, translatedUserId); + } else { + mInterface.deleteApplicationCacheFilesAsUser(pkg, translatedUserId, obs); + } synchronized (obs) { while (!obs.finished) { try { @@ -4042,8 +4060,10 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --user: remove the app from the given user."); pw.println(" --versionCode: only uninstall if the app has the given version code."); pw.println(""); - pw.println(" clear [--user USER_ID] PACKAGE"); - pw.println(" Deletes all data associated with a package."); + pw.println(" clear [--user USER_ID] [--cache-only] PACKAGE"); + pw.println(" Deletes data associated with a package. Options are:"); + pw.println(" --user: specifies the user for which we need to clear data"); + pw.println(" --cache-only: a flag which tells if we only need to clear cache data"); pw.println(""); pw.println(" enable [--user USER_ID] PACKAGE_OR_COMPONENT"); pw.println(" disable [--user USER_ID] PACKAGE_OR_COMPONENT"); diff --git a/services/core/java/com/android/server/pm/ProtectedPackages.java b/services/core/java/com/android/server/pm/ProtectedPackages.java index bf4612973023..e9239889973a 100644 --- a/services/core/java/com/android/server/pm/ProtectedPackages.java +++ b/services/core/java/com/android/server/pm/ProtectedPackages.java @@ -16,11 +16,11 @@ package com.android.server.pm; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.Context; import android.os.UserHandle; -import android.util.ArrayMap; import android.util.ArraySet; import android.util.SparseArray; @@ -56,7 +56,7 @@ public class ProtectedPackages { @Nullable @GuardedBy("this") - private final ArrayMap<String, Set<String>> mDeviceOwnerProtectedPackages = new ArrayMap<>(); + private final SparseArray<Set<String>> mOwnerProtectedPackages = new SparseArray<>(); private final Context mContext; @@ -79,13 +79,13 @@ public class ProtectedPackages { : profileOwnerPackages.clone(); } - /** Sets the protected packages for the device owner. */ - public synchronized void setDeviceOwnerProtectedPackages( - String deviceOwnerPackageName, List<String> packageNames) { + /** Sets packages protected by a device or profile owner. */ + public synchronized void setOwnerProtectedPackages( + @UserIdInt int userId, @NonNull List<String> packageNames) { if (packageNames.isEmpty()) { - mDeviceOwnerProtectedPackages.remove(deviceOwnerPackageName); + mOwnerProtectedPackages.remove(userId); } else { - mDeviceOwnerProtectedPackages.put(deviceOwnerPackageName, new ArraySet<>(packageNames)); + mOwnerProtectedPackages.put(userId, new ArraySet<>(packageNames)); } } @@ -123,19 +123,24 @@ public class ProtectedPackages { * <p>A protected package means that, apart from the package owner, no system or privileged apps * can modify its data or package state. */ - private synchronized boolean isProtectedPackage(String packageName) { + private synchronized boolean isProtectedPackage(@UserIdInt int userId, String packageName) { return packageName != null && (packageName.equals(mDeviceProvisioningPackage) - || isDeviceOwnerProtectedPackage(packageName)); + || isOwnerProtectedPackage(userId, packageName)); } - /** Returns {@code true} if the given package is a protected package set by any device owner. */ - private synchronized boolean isDeviceOwnerProtectedPackage(String packageName) { - for (Set<String> protectedPackages : mDeviceOwnerProtectedPackages.values()) { - if (protectedPackages.contains(packageName)) { - return true; - } - } - return false; + /** + * Returns {@code true} if the given package is a protected package set by any device or + * profile owner. + */ + private synchronized boolean isOwnerProtectedPackage( + @UserIdInt int userId, String packageName) { + return isPackageProtectedForUser(UserHandle.USER_ALL, packageName) + || isPackageProtectedForUser(userId, packageName); + } + + private synchronized boolean isPackageProtectedForUser(int userId, String packageName) { + int userIdx = mOwnerProtectedPackages.indexOfKey(userId); + return userIdx >= 0 && mOwnerProtectedPackages.valueAt(userIdx).contains(packageName); } /** @@ -146,7 +151,7 @@ public class ProtectedPackages { */ public boolean isPackageStateProtected(@UserIdInt int userId, String packageName) { return hasDeviceOwnerOrProfileOwner(userId, packageName) - || isProtectedPackage(packageName); + || isProtectedPackage(userId, packageName); } /** @@ -155,6 +160,6 @@ public class ProtectedPackages { */ public boolean isPackageDataProtected(@UserIdInt int userId, String packageName) { return hasDeviceOwnerOrProfileOwner(userId, packageName) - || isProtectedPackage(packageName); + || isProtectedPackage(userId, packageName); } } diff --git a/services/core/java/com/android/server/pm/ResolveIntentHelper.java b/services/core/java/com/android/server/pm/ResolveIntentHelper.java index b74670b77a11..2db1c2029d9c 100644 --- a/services/core/java/com/android/server/pm/ResolveIntentHelper.java +++ b/services/core/java/com/android/server/pm/ResolveIntentHelper.java @@ -306,16 +306,32 @@ final class ResolveIntentHelper { return new IntentSender(target); } - // In this method, we have to know the actual calling UID, but in some cases Binder's - // call identity is removed, so the UID has to be passed in explicitly. - public @NonNull List<ResolveInfo> queryIntentReceiversInternal(Computer computer, Intent intent, + /** + * Retrieve all receivers that can handle a broadcast of the given intent. + * @param queryingUid the results will be filtered in the context of this UID instead. + */ + @NonNull + public List<ResolveInfo> queryIntentReceiversInternal(Computer computer, Intent intent, + String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, int userId, + int queryingUid) { + return queryIntentReceiversInternal(computer, intent, resolvedType, flags, userId, + queryingUid, false); + } + + /** + * @see PackageManagerInternal#queryIntentReceivers(Intent, String, long, int, int, boolean) + */ + @NonNull + public List<ResolveInfo> queryIntentReceiversInternal(Computer computer, Intent intent, String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, int userId, - int filterCallingUid) { + int filterCallingUid, boolean forSend) { if (!mUserManager.exists(userId)) return Collections.emptyList(); - computer.enforceCrossUserPermission(filterCallingUid, userId, false /*requireFullPermission*/, - false /*checkShell*/, "query intent receivers"); - final String instantAppPkgName = computer.getInstantAppPackageName(filterCallingUid); - flags = computer.updateFlagsForResolve(flags, userId, filterCallingUid, + // The identity used to filter the receiver components + final int queryingUid = forSend ? Process.SYSTEM_UID : filterCallingUid; + computer.enforceCrossUserPermission(queryingUid, userId, + false /*requireFullPermission*/, false /*checkShell*/, "query intent receivers"); + final String instantAppPkgName = computer.getInstantAppPackageName(queryingUid); + flags = computer.updateFlagsForResolve(flags, userId, queryingUid, false /*includeInstantApps*/, computer.isImplicitImageCaptureIntentAndNotSetByDpc(intent, userId, resolvedType, flags)); @@ -397,7 +413,7 @@ final class ResolveIntentHelper { list, true, originalIntent, resolvedType, filterCallingUid); } - return computer.applyPostResolutionFilter(list, instantAppPkgName, false, filterCallingUid, + return computer.applyPostResolutionFilter(list, instantAppPkgName, false, queryingUid, false, userId, intent); } diff --git a/services/core/java/com/android/server/pm/ThreadComputer.java b/services/core/java/com/android/server/pm/ThreadComputer.java deleted file mode 100644 index f603e635f75d..000000000000 --- a/services/core/java/com/android/server/pm/ThreadComputer.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2021 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.server.pm; - -/** - * This class records the Computer being used by a thread and the Computer's reference - * count. There is a thread-local copy of this class. - */ -public final class ThreadComputer implements AutoCloseable { - Computer mComputer = null; - int mRefCount = 0; - - void acquire(Computer c) { - if (mRefCount != 0 && mComputer != c) { - throw new RuntimeException("computer mismatch, count = " + mRefCount); - } - mComputer = c; - mRefCount++; - } - - void acquire() { - if (mRefCount == 0 || mComputer == null) { - throw new RuntimeException("computer acquire on empty ref count"); - } - mRefCount++; - } - - void release() { - if (--mRefCount == 0) { - mComputer = null; - } - } - - @Override - public void close() { - release(); - } -} diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java index 95482d7c7f1a..974a1e1cd59d 100644 --- a/services/core/java/com/android/server/pm/UserDataPreparer.java +++ b/services/core/java/com/android/server/pm/UserDataPreparer.java @@ -70,9 +70,16 @@ class UserDataPreparer { void prepareUserData(int userId, int userSerial, int flags) { synchronized (mInstallLock) { final StorageManager storage = mContext.getSystemService(StorageManager.class); + /* + * Internal storage must be prepared before adoptable storage, since the user's volume + * keys are stored in their internal storage. + */ + prepareUserDataLI(null /* internal storage */, userId, userSerial, flags, true); for (VolumeInfo vol : storage.getWritablePrivateVolumes()) { final String volumeUuid = vol.getFsUuid(); - prepareUserDataLI(volumeUuid, userId, userSerial, flags, true); + if (volumeUuid != null) { + prepareUserDataLI(volumeUuid, userId, userSerial, flags, true); + } } } } @@ -136,10 +143,17 @@ class UserDataPreparer { void destroyUserData(int userId, int flags) { synchronized (mInstallLock) { final StorageManager storage = mContext.getSystemService(StorageManager.class); + /* + * Volume destruction order isn't really important, but to avoid any weird issues we + * process internal storage last, the opposite of prepareUserData. + */ for (VolumeInfo vol : storage.getWritablePrivateVolumes()) { final String volumeUuid = vol.getFsUuid(); - destroyUserDataLI(volumeUuid, userId, flags); + if (volumeUuid != null) { + destroyUserDataLI(volumeUuid, userId, flags); + } } + destroyUserDataLI(null /* internal storage */, userId, flags); } } diff --git a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java index b26b6940a1af..d49227dec454 100644 --- a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java +++ b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java @@ -22,11 +22,13 @@ import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILATI import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILE_FILTER__ART_COMPILATION_FILTER_FAKE_RUN_FROM_APK_FALLBACK; import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILE_FILTER__ART_COMPILATION_FILTER_FAKE_RUN_FROM_VDEX_FALLBACK; +import android.app.job.JobParameters; import android.os.SystemClock; import android.util.Slog; import android.util.jar.StrictJarFile; import com.android.internal.art.ArtStatsLog; +import com.android.server.pm.BackgroundDexOptService; import com.android.server.pm.PackageManagerService; import java.io.IOException; @@ -299,4 +301,31 @@ public class ArtStatsLogUtils { ArtStatsLog.ART_DATUM_REPORTED__ISA__ART_ISA_UNKNOWN)); } } + + private static final Map<Integer, Integer> STATUS_MAP = + Map.of(BackgroundDexOptService.STATUS_OK, + ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED, + BackgroundDexOptService.STATUS_ABORT_BY_CANCELLATION, + ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_CANCELLATION, + BackgroundDexOptService.STATUS_ABORT_NO_SPACE_LEFT, + ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_NO_SPACE_LEFT, + BackgroundDexOptService.STATUS_ABORT_THERMAL, + ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_THERMAL, + BackgroundDexOptService.STATUS_ABORT_BATTERY, + ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BATTERY, + BackgroundDexOptService.STATUS_DEX_OPT_FAILED, + ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED); + + /** Helper class to write background dexopt job stats to statsd. */ + public static class BackgroundDexoptJobStatsLogger { + /** Writes background dexopt job stats to statsd. */ + public void write(@BackgroundDexOptService.Status int status, + @JobParameters.StopReason int cancellationReason, long durationMs, + long durationIncludingSleepMs) { + ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED, + STATUS_MAP.getOrDefault(status, + ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN), + cancellationReason, durationMs, durationIncludingSleepMs); + } + } } diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index f727c11879b0..67960655b618 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -57,7 +57,6 @@ import android.provider.Telephony.Sms.Intents; import android.security.Credentials; import android.speech.RecognitionService; import android.telephony.TelephonyManager; -import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -618,6 +617,10 @@ final class DefaultPermissionGrantPolicy { grantPermissionsToSystemPackage(pm, getDefaultSearchSelectorPackage(), userId, NOTIFICATION_PERMISSIONS); + // Captive Portal Login + grantPermissionsToSystemPackage(pm, getDefaultCaptivePortalLoginPackage(), userId, + NOTIFICATION_PERMISSIONS); + // Camera grantPermissionsToSystemPackage(pm, getDefaultSystemHandlerActivityPackage(pm, MediaStore.ACTION_IMAGE_CAPTURE, userId), @@ -909,14 +912,6 @@ final class DefaultPermissionGrantPolicy { grantSystemFixedPermissionsToSystemPackage(pm, "com.android.sharedstoragebackup", userId, STORAGE_PERMISSIONS); - // System Captions Service - String systemCaptionsServicePackageName = - mContext.getPackageManager().getSystemCaptionsServicePackageName(); - if (!TextUtils.isEmpty(systemCaptionsServicePackageName)) { - grantPermissionsToSystemPackage(pm, systemCaptionsServicePackageName, userId, - MICROPHONE_PERMISSIONS); - } - // Bluetooth MIDI Service grantSystemFixedPermissionsToSystemPackage(pm, MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE, userId, @@ -933,6 +928,10 @@ final class DefaultPermissionGrantPolicy { return mContext.getString(R.string.config_defaultSearchSelectorPackageName); } + private String getDefaultCaptivePortalLoginPackage() { + return mContext.getString(R.string.config_defaultCaptivePortalLoginPackageName); + } + @SafeVarargs private final void grantPermissionToEachSystemPackage(PackageManagerWrapper pm, ArrayList<String> packages, int userId, Set<String>... permissions) { diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java index 446e20b70279..6c2354f1b3e6 100644 --- a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java +++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java @@ -17,6 +17,7 @@ package com.android.server.pm.permission; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; /** @@ -105,6 +106,20 @@ public interface LegacyPermissionManagerInternal { void scheduleReadDefaultPermissionExceptions(); /** + * Check whether a particular package should have access to the microphone data from the + * SoundTrigger. + * + * @param uid the uid of the package you are checking against + * @param packageName the name of the package you are checking against + * @param attributionTag the attributionTag to attach to the app op transaction + * @param reason the reason to attach to the app op transaction + * @return {@code PERMISSION_GRANTED} if the permission is granted, + * or {@code PERMISSION_SOFT/HARD DENIED otherwise + */ + int checkSoundTriggerRecordAudioPermissionForDataDelivery(int uid, + @NonNull String packageName, @Nullable String attributionTag, @NonNull String reason); + + /** * Provider for package names. */ interface PackagesProvider { diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java index 360a04f7e9bc..88b4a94f7027 100644 --- a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java @@ -16,12 +16,15 @@ package com.android.server.pm.permission; +import static android.Manifest.permission.RECORD_AUDIO; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.PermissionChecker; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; @@ -399,6 +402,21 @@ public class LegacyPermissionManagerService extends ILegacyPermissionManager.Stu public void scheduleReadDefaultPermissionExceptions() { mDefaultPermissionGrantPolicy.scheduleReadDefaultPermissionExceptions(); } + + @Override + public int checkSoundTriggerRecordAudioPermissionForDataDelivery(int uid, + @NonNull String packageName, @Nullable String attributionTag, + @NonNull String reason) { + int result = PermissionChecker.checkPermissionForPreflight(mContext, RECORD_AUDIO, -1, + uid, packageName); + if (result != PermissionChecker.PERMISSION_GRANTED) { + return result; + } + mContext.getSystemService(AppOpsManager.class).noteOpNoThrow( + AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, uid, packageName, + attributionTag, reason); + return result; + } } /** diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 5a05134bed81..a83cb5e37ba2 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -17,7 +17,6 @@ package com.android.server.pm.permission; import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD; -import static android.Manifest.permission.POST_NOTIFICATIONS; import static android.Manifest.permission.RECORD_AUDIO; import static android.Manifest.permission.UPDATE_APP_OPS_STATS; import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; @@ -51,7 +50,6 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.permission.SplitPermissionInfoParcelable; import android.os.Binder; -import android.os.Build; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; @@ -596,26 +594,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { } @Override - public int checkPostNotificationsPermissionGrantedOrLegacyAccess(int uid) { - int granted = PermissionManagerService.this.checkUidPermission(uid, - POST_NOTIFICATIONS); - AndroidPackage pkg = mPackageManagerInt.getPackage(uid); - if (pkg == null) { - Slog.e(LOG_TAG, "No package for uid " + uid); - return granted; - } - if (granted != PackageManager.PERMISSION_GRANTED - && pkg.getTargetSdkVersion() >= Build.VERSION_CODES.M) { - int flags = PermissionManagerService.this.getPermissionFlags(pkg.getPackageName(), - POST_NOTIFICATIONS, UserHandle.getUserId(uid)); - if ((flags & PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) != 0) { - return PackageManager.PERMISSION_GRANTED; - } - } - return granted; - } - - @Override public void startShellPermissionIdentityDelegation(int uid, @NonNull String packageName, @Nullable List<String> permissionNames) { Objects.requireNonNull(packageName, "packageName"); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 7be83b03243a..d34682df3413 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -42,6 +42,7 @@ import static android.content.pm.PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM import static android.content.pm.PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE; import static android.content.pm.PackageManager.MASK_PERMISSION_FLAGS_ALL; import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.INVALID_UID; import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; import static android.permission.PermissionManager.KILL_APP_REASON_GIDS_CHANGED; @@ -97,6 +98,7 @@ import android.os.storage.StorageManager; import android.permission.IOnPermissionsChangeListener; import android.permission.PermissionControllerManager; import android.permission.PermissionManager; +import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -333,7 +335,8 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt mPackageManagerInt.writeSettings(true); } @Override - public void onPermissionRevoked(int uid, int userId, String reason, boolean overrideKill) { + public void onPermissionRevoked(int uid, int userId, String reason, boolean overrideKill, + @Nullable String permissionName) { mOnPermissionChangeListeners.onPermissionsChanged(uid); // Critical; after this call the application should never have the permission @@ -342,13 +345,41 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt return; } - final int appId = UserHandle.getAppId(uid); - if (reason == null) { - mHandler.post(() -> killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED)); - } else { - mHandler.post(() -> killUid(appId, userId, reason)); + mHandler.post(() -> { + if (POST_NOTIFICATIONS.equals(permissionName) + && isAppBackupAndRestoreRunning(uid)) { + return; + } + + final int appId = UserHandle.getAppId(uid); + if (reason == null) { + killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED); + } else { + killUid(appId, userId, reason); + } + }); + } + + private boolean isAppBackupAndRestoreRunning(int uid) { + if (checkUidPermission(uid, Manifest.permission.BACKUP) != PERMISSION_GRANTED) { + return false; + } + + try { + int userId = UserHandle.getUserId(uid); + boolean isInSetup = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, userId) == 0; + boolean isInDeferredSetup = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId) + == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED; + return isInSetup || isInDeferredSetup; + } catch (Settings.SettingNotFoundException e) { + Slog.w(LOG_TAG, "Failed to check if the user is in restore: " + e); + return false; } } + @Override public void onInstallPermissionRevoked() { mPackageManagerInt.writeSettings(true); @@ -1600,7 +1631,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt if (callback != null) { if (isRuntimePermission) { callback.onPermissionRevoked(UserHandle.getUid(userId, pkg.getUid()), userId, - reason, overrideKill); + reason, overrideKill, permName); } else { mDefaultPermissionCallback.onInstallPermissionRevoked(); } @@ -5246,7 +5277,11 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt onPermissionRevoked(uid, userId, reason, false); } public void onPermissionRevoked(int uid, @UserIdInt int userId, String reason, - boolean overrideKill) {} + boolean overrideKill) { + onPermissionRevoked(uid, userId, reason, false, null); + } + public void onPermissionRevoked(int uid, @UserIdInt int userId, String reason, + boolean overrideKill, @Nullable String permissionName) {} public void onInstallPermissionRevoked() {} public void onPermissionUpdated(@UserIdInt int[] updatedUserIds, boolean sync) {} public void onPermissionUpdatedNotifyListener(@UserIdInt int[] updatedUserIds, boolean sync, diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java index 812d7a04dc13..d2c4ec4cc5a5 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java @@ -63,17 +63,6 @@ public interface PermissionManagerServiceInternal extends PermissionManagerInter int checkUidPermission(int uid, @NonNull String permissionName); /** - * Check whether a particular UID has been granted the POST_NOTIFICATIONS permission, or if - * access should be granted based on legacy access (currently symbolized by the REVIEW_REQUIRED - * permission flag - * - * @param uid the UID - * @return {@code PERMISSION_GRANTED} if the permission is granted, or legacy access is granted, - * {@code PERMISSION_DENIED} otherwise - */ - int checkPostNotificationsPermissionGrantedOrLegacyAccess(int uid); - - /** * Adds a listener for runtime permission state (permissions or flags) changes. * * @param listener The listener. diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java b/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java index f2e2f4f009a9..281e1bd2824d 100644 --- a/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java +++ b/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java @@ -26,6 +26,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.ArrayMap; +import android.util.EventLog; import android.util.Slog; import com.android.internal.R; @@ -36,6 +37,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.List; +import java.util.Objects; /** * @hide @@ -277,8 +279,28 @@ public class ParsedPermissionUtils { } /** - * @return {@code true} if the package declares duplicate permissions with different - * protection levels. + * Determines if a duplicate permission is malformed .i.e. defines different protection level + * or group. + */ + private static boolean isMalformedDuplicate(ParsedPermission p1, ParsedPermission p2) { + // Since a permission tree is also added as a permission with normal protection + // level, we need to skip if the parsedPermission is a permission tree. + if (p1 == null || p2 == null || p1.isTree() || p2.isTree()) { + return false; + } + + if (p1.getProtectionLevel() != p2.getProtectionLevel()) { + return true; + } + if (!Objects.equals(p1.getGroup(), p2.getGroup())) { + return true; + } + + return false; + } + + /** + * @return {@code true} if the package declares malformed duplicate permissions. */ public static boolean declareDuplicatePermission(@NonNull ParsingPackage pkg) { final List<ParsedPermission> permissions = pkg.getPermissions(); @@ -289,10 +311,10 @@ public class ParsedPermissionUtils { final ParsedPermission parsedPermission = permissions.get(i); final String name = parsedPermission.getName(); final ParsedPermission perm = checkDuplicatePerm.get(name); - // Since a permission tree is also added as a permission with normal protection - // level, we need to skip if the parsedPermission is a permission tree. - if (perm != null && !(perm.isTree() || parsedPermission.isTree()) - && perm.getProtectionLevel() != parsedPermission.getProtectionLevel()) { + if (isMalformedDuplicate(parsedPermission, perm)) { + // Fix for b/213323615 + EventLog.writeEvent(0x534e4554, "213323615", + "The package " + pkg.getPackageName() + " seems malicious"); return true; } checkDuplicatePerm.put(name, parsedPermission); diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index 6ee9c66e328a..06a54a461d5e 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -968,7 +968,7 @@ public class ParsingPackageUtils { if (ParsedPermissionUtils.declareDuplicatePermission(pkg)) { return input.error( INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, - "Declare duplicate permissions with different protection levels." + "Found duplicate permission with a different attribute value." ); } diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java index e157a277366f..a6d148c824c9 100644 --- a/services/core/java/com/android/server/policy/AppOpsPolicy.java +++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java @@ -87,8 +87,8 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat private final VoiceInteractionManagerInternal mVoiceInteractionManagerInternal; /** - * Whether this device allows only the HotwordDetectionService to use OP_RECORD_AUDIO_HOTWORD - * which doesn't incur the privacy indicator. + * Whether this device allows only the HotwordDetectionService to use + * OP_RECORD_AUDIO_HOTWORD which doesn't incur the privacy indicator. */ private final boolean mIsHotwordDetectionServiceRequired; @@ -428,8 +428,8 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat if (!mIsHotwordDetectionServiceRequired) { return code; } - // Only the HotwordDetectionService can use the HOTWORD op which doesn't incur the - // privacy indicator. Downgrade to standard RECORD_AUDIO for other processes. + // Only the HotwordDetectionService can use the RECORD_AUDIO_HOTWORD op which doesn't + // incur the privacy indicator. Downgrade to standard RECORD_AUDIO for other processes. final HotwordDetectionServiceIdentity hotwordDetectionServiceIdentity = mVoiceInteractionManagerInternal.getHotwordDetectionServiceIdentity(); if (hotwordDetectionServiceIdentity != null diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java index 977f79f6175d..b56e1120f16a 100644 --- a/services/core/java/com/android/server/policy/PermissionPolicyService.java +++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java @@ -915,8 +915,7 @@ public final class PermissionPolicyService extends SystemService { int permissionFlags = mPackageManager.getPermissionFlags(permissionName, packageName, mContext.getUser()); boolean isReviewRequired = (permissionFlags & FLAG_PERMISSION_REVIEW_REQUIRED) != 0; - if (isReviewRequired && !CompatChanges.isChangeEnabled( - NOTIFICATION_PERM_CHANGE_ID, packageName, user)) { + if (isReviewRequired) { return; } @@ -1118,48 +1117,13 @@ public final class PermissionPolicyService extends SystemService { private class Internal extends PermissionPolicyInternal { - // UIDs that, if a grant dialog is shown for POST_NOTIFICATIONS before next reboot, - // should display a "continue allowing" message, rather than an "allow" message - private final ArraySet<Integer> mContinueNotifGrantMessageUids = new ArraySet<>(); - private final ActivityInterceptorCallback mActivityInterceptorCallback = new ActivityInterceptorCallback() { @Nullable @Override public ActivityInterceptorCallback.ActivityInterceptResult intercept( ActivityInterceptorInfo info) { - String action = info.intent.getAction(); - ActivityInterceptResult result = null; - if (!ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action) - && !PackageManager.ACTION_REQUEST_PERMISSIONS.equals(action)) { - return null; - } - // Only this interceptor can add LEGACY_ACCESS_PERMISSION_NAMES - if (info.intent.getStringArrayExtra(PackageManager - .EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES) - != null) { - result = new ActivityInterceptResult( - new Intent(info.intent), info.checkedOptions); - result.intent.removeExtra(PackageManager - .EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES); - } - if (PackageManager.ACTION_REQUEST_PERMISSIONS.equals(action) - && !mContinueNotifGrantMessageUids.contains(info.realCallingUid)) { - return result; - } - if (ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action)) { - String otherPkg = info.intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); - if (otherPkg == null || (mPackageManager.getPermissionFlags( - POST_NOTIFICATIONS, otherPkg, UserHandle.of(info.userId)) - & FLAG_PERMISSION_REVIEW_REQUIRED) == 0) { - return result; - } - } - - mContinueNotifGrantMessageUids.remove(info.realCallingUid); - return new ActivityInterceptResult(info.intent.putExtra(PackageManager - .EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES, - new String[] { POST_NOTIFICATIONS }), info.checkedOptions); + return null; } @Override @@ -1173,10 +1137,8 @@ public final class PermissionPolicyService extends SystemService { return; } UserHandle user = UserHandle.of(taskInfo.userId); - if (CompatChanges.isChangeEnabled(NOTIFICATION_PERM_CHANGE_ID, + if (!CompatChanges.isChangeEnabled(NOTIFICATION_PERM_CHANGE_ID, activityInfo.packageName, user)) { - clearNotificationReviewFlagsIfNeeded(activityInfo.packageName, user); - } else { // Post the activity start checks to ensure the notification channel // checks happen outside the WindowManager global lock. mHandler.post(() -> showNotificationPromptIfNeeded( @@ -1337,22 +1299,6 @@ public final class PermissionPolicyService extends SystemService { && isLauncherIntent(taskInfo.baseIntent); } - private void clearNotificationReviewFlagsIfNeeded(String packageName, UserHandle user) { - if ((mPackageManager.getPermissionFlags(POST_NOTIFICATIONS, packageName, user) - & FLAG_PERMISSION_REVIEW_REQUIRED) == 0) { - return; - } - try { - int uid = mPackageManager.getPackageUidAsUser(packageName, 0, - user.getIdentifier()); - mContinueNotifGrantMessageUids.add(uid); - mPackageManager.updatePermissionFlags(POST_NOTIFICATIONS, packageName, - FLAG_PERMISSION_REVIEW_REQUIRED, 0, user); - } catch (PackageManager.NameNotFoundException e) { - // Do nothing - } - } - private void launchNotificationPermissionRequestDialog(String pkgName, UserHandle user, int taskId, @Nullable ActivityInterceptorInfo info) { Intent grantPermission = mPackageManager @@ -1469,8 +1415,7 @@ public final class PermissionPolicyService extends SystemService { == PackageManager.PERMISSION_GRANTED; int flags = mPackageManager.getPermissionFlags(POST_NOTIFICATIONS, pkgName, user); boolean explicitlySet = (flags & PermissionManager.EXPLICIT_SET_FLAGS) != 0; - boolean needsReview = (flags & FLAG_PERMISSION_REVIEW_REQUIRED) != 0; - return !granted && hasCreatedNotificationChannels && (needsReview || !explicitlySet); + return !granted && hasCreatedNotificationChannels && !explicitlySet; } } } diff --git a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java index 750d4004cb60..fd6ec065d421 100644 --- a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java +++ b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java @@ -16,44 +16,105 @@ package com.android.server.sensorprivacy; +import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL; + +import android.annotation.ColorInt; import android.app.AppOpsManager; import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; import android.hardware.lights.Light; import android.hardware.lights.LightState; import android.hardware.lights.LightsManager; import android.hardware.lights.LightsRequest; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.Looper; +import android.os.SystemClock; import android.permission.PermissionManager; import android.util.ArraySet; +import android.util.Pair; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.FgThread; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; + +class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener, + SensorEventListener { -class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener { + @VisibleForTesting + static final double LIGHT_VALUE_MULTIPLIER = 1 / Math.log(1.1); + private final Handler mHandler; + private final Executor mExecutor; private final Context mContext; + private final AppOpsManager mAppOpsManager; private final LightsManager mLightsManager; + private final SensorManager mSensorManager; private final Set<String> mActivePackages = new ArraySet<>(); private final Set<String> mActivePhonePackages = new ArraySet<>(); - private final int mCameraPrivacyLightColor; - private final List<Light> mCameraLights = new ArrayList<>(); - private final AppOpsManager mAppOpsManager; private LightsManager.LightsSession mLightsSession = null; + @ColorInt + private final int mDayColor; + @ColorInt + private final int mNightColor; + + private final Sensor mLightSensor; + + private boolean mIsAmbientLightListenerRegistered = false; + private final long mMovingAverageIntervalMillis; + /** When average of the time integral over the past {@link #mMovingAverageIntervalMillis} + * milliseconds of the log_1.1(lux(t)) is greater than this value, use the daytime brightness + * else use nighttime brightness. */ + private final long mNightThreshold; + private final ArrayDeque<Pair<Long, Integer>> mAmbientLightValues = new ArrayDeque<>(); + /** Tracks the Riemann sum of {@link #mAmbientLightValues} to avoid O(n) operations when sum is + * needed */ + private long mAlvSum = 0; + private int mLastLightColor = 0; + /** The elapsed real time that the ALS was started watching */ + private long mElapsedTimeStartedReading; + + private final Object mDelayedUpdateToken = new Object(); + + // Can't mock static native methods, workaround for testing + private long mElapsedRealTime = -1; + CameraPrivacyLightController(Context context) { + this(context, FgThread.get().getLooper()); + } + + @VisibleForTesting + CameraPrivacyLightController(Context context, Looper looper) { mContext = context; + mHandler = new Handler(looper); + mExecutor = new HandlerExecutor(mHandler); + mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mLightsManager = mContext.getSystemService(LightsManager.class); + mSensorManager = mContext.getSystemService(SensorManager.class); - mCameraPrivacyLightColor = mContext.getColor(R.color.camera_privacy_light); + mDayColor = mContext.getColor(R.color.camera_privacy_light_day); + mNightColor = mContext.getColor(R.color.camera_privacy_light_night); + mMovingAverageIntervalMillis = mContext.getResources() + .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis); + mNightThreshold = (long) (Math.log(mContext.getResources() + .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold)) + * LIGHT_VALUE_MULTIPLIER); List<Light> lights = mLightsManager.getLights(); for (int i = 0; i < lights.size(); i++) { @@ -64,12 +125,60 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis } if (mCameraLights.isEmpty()) { + mLightSensor = null; return; } mAppOpsManager.startWatchingActive( new String[] {AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_PHONE_CALL_CAMERA}, - FgThread.getExecutor(), this); + mExecutor, this); + + // It may be useful in the future to configure devices to know which lights are near which + // sensors so that we can control individual lights based on their environment. + mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); + } + + private void addElement(long time, int value) { + if (mAmbientLightValues.isEmpty()) { + // Eliminate the size == 1 edge case and assume the light value has been constant for + // the previous interval + mAmbientLightValues.add(new Pair<>(time - getCurrentIntervalMillis() - 1, value)); + } + Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast(); + mAmbientLightValues.add(new Pair<>(time, value)); + + mAlvSum += (time - lastElement.first) * lastElement.second; + removeObsoleteData(time); + } + + private void removeObsoleteData(long time) { + while (mAmbientLightValues.size() > 1) { + Pair<Long, Integer> element0 = mAmbientLightValues.pollFirst(); // NOTICE: POLL + Pair<Long, Integer> element1 = mAmbientLightValues.peekFirst(); // NOTICE: PEEK + if (element1.first > time - getCurrentIntervalMillis()) { + mAmbientLightValues.addFirst(element0); + break; + } + mAlvSum -= (element1.first - element0.first) * element0.second; + } + } + + /** + * Gives the Riemann sum of {@link #mAmbientLightValues} where the part of the interval that + * stretches outside the time window is removed and the time since the last change is added in. + */ + private long getLiveAmbientLightTotal() { + if (mAmbientLightValues.isEmpty()) { + return mAlvSum; + } + long time = getElapsedRealTime(); + removeObsoleteData(time); + + Pair<Long, Integer> firstElement = mAmbientLightValues.peekFirst(); + Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast(); + + return mAlvSum - Math.max(0, time - getCurrentIntervalMillis() - firstElement.first) + * firstElement.second + (time - lastElement.first) * lastElement.second; } @Override @@ -93,10 +202,16 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis } private void updateLightSession() { + if (Looper.myLooper() != mHandler.getLooper()) { + mHandler.post(this::updateLightSession); + return; + } + Set<String> exemptedPackages = PermissionManager.getIndicatorExemptedPackages(mContext); boolean shouldSessionEnd = exemptedPackages.containsAll(mActivePackages) && exemptedPackages.containsAll(mActivePhonePackages); + updateSensorListener(shouldSessionEnd); if (shouldSessionEnd) { if (mLightsSession == null) { @@ -106,20 +221,73 @@ class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedLis mLightsSession.close(); mLightsSession = null; } else { - if (mLightsSession != null) { + int lightColor; + if (mLightSensor != null && getLiveAmbientLightTotal() + < getCurrentIntervalMillis() * mNightThreshold) { + lightColor = mNightColor; + } else { + lightColor = mDayColor; + } + + if (mLastLightColor == lightColor && mLightsSession != null) { return; } + mLastLightColor = lightColor; LightsRequest.Builder requestBuilder = new LightsRequest.Builder(); for (int i = 0; i < mCameraLights.size(); i++) { requestBuilder.addLight(mCameraLights.get(i), new LightState.Builder() - .setColor(mCameraPrivacyLightColor) + .setColor(lightColor) .build()); } - mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE); + if (mLightsSession == null) { + mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE); + } + mLightsSession.requestLights(requestBuilder.build()); } } + + private void updateSensorListener(boolean shouldSessionEnd) { + if (shouldSessionEnd && mIsAmbientLightListenerRegistered) { + mSensorManager.unregisterListener(this); + mIsAmbientLightListenerRegistered = false; + } + if (!shouldSessionEnd && !mIsAmbientLightListenerRegistered && mLightSensor != null) { + mSensorManager.registerListener(this, mLightSensor, SENSOR_DELAY_NORMAL, mHandler); + mIsAmbientLightListenerRegistered = true; + mElapsedTimeStartedReading = getElapsedRealTime(); + } + } + + private long getElapsedRealTime() { + return mElapsedRealTime == -1 ? SystemClock.elapsedRealtime() : mElapsedRealTime; + } + + @VisibleForTesting + void setElapsedRealTime(long time) { + mElapsedRealTime = time; + } + + @Override + public void onSensorChanged(SensorEvent event) { + // Using log space to represent human sensation (Fechner's Law) instead of lux + // because lux values causes bright flashes to skew the average very high. + addElement(event.timestamp, Math.max(0, + (int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER))); + updateLightSession(); + mHandler.removeCallbacksAndMessages(mDelayedUpdateToken); + mHandler.postDelayed(CameraPrivacyLightController.this::updateLightSession, + mDelayedUpdateToken, mMovingAverageIntervalMillis); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + private long getCurrentIntervalMillis() { + return Math.min(mMovingAverageIntervalMillis, + getElapsedRealTime() - mElapsedTimeStartedReading); + } } diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java index 3c779f387673..c354f116af5f 100644 --- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java +++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java @@ -25,6 +25,7 @@ import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA; import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE; +import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.content.Intent.EXTRA_PACKAGE_NAME; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; @@ -1067,6 +1068,10 @@ public final class SensorPrivacyService extends SystemService { mAppOpsRestrictionToken); mAppOpsManagerInternal.setGlobalRestriction(OP_PHONE_CALL_MICROPHONE, enabled, mAppOpsRestrictionToken); + // We don't show the dialog for RECEIVE_SOUNDTRIGGER_AUDIO, but still want to + // restrict it when the microphone is disabled + mAppOpsManagerInternal.setGlobalRestriction(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, + enabled, mAppOpsRestrictionToken); break; case CAMERA: mAppOpsManagerInternal.setGlobalRestriction(OP_CAMERA, enabled, diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java index f3d151fe5cc4..13fe14caa1f9 100644 --- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java +++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java @@ -41,6 +41,9 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceSpecificException; +import com.android.server.LocalServices; +import com.android.server.pm.permission.LegacyPermissionManagerInternal; + import java.io.PrintWriter; import java.util.Objects; @@ -120,8 +123,7 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware * Throws a {@link SecurityException} iff the originator has permission to receive data. */ void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) { - enforcePermissionForDataDelivery(mContext, identity, RECORD_AUDIO, - reason); + enforceSoundTriggerRecordAudioPermissionForDataDelivery(identity, reason); enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD, reason); } @@ -147,6 +149,19 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware } } + private static void enforceSoundTriggerRecordAudioPermissionForDataDelivery( + @NonNull Identity identity, @NonNull String reason) { + LegacyPermissionManagerInternal lpmi = + LocalServices.getService(LegacyPermissionManagerInternal.class); + final int status = lpmi.checkSoundTriggerRecordAudioPermissionForDataDelivery(identity.uid, + identity.packageName, identity.attributionTag, reason); + if (status != PermissionChecker.PERMISSION_GRANTED) { + throw new SecurityException( + String.format("Failed to obtain permission RECORD_AUDIO for identity %s", + ObjectPrinter.print(identity, 16))); + } + } + /** * Throws a {@link SecurityException} if originator permanently doesn't have the given * permission. diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index 977f6fd7b528..06646d30eb7b 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -4366,7 +4366,8 @@ public class StatsPullAtomService extends SystemService { } RkpErrorStats atom = atomWrapper.payload.getRkpErrorStats(); pulledData.add(FrameworkStatsLog.buildStatsEvent( - FrameworkStatsLog.RKP_ERROR_STATS, atom.rkpError, atomWrapper.count)); + FrameworkStatsLog.RKP_ERROR_STATS, atom.rkpError, atomWrapper.count, + atom.security_level)); } return StatsManager.PULL_SUCCESS; } diff --git a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java index 08e8eebb8740..999d4064c951 100644 --- a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java +++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java @@ -23,6 +23,8 @@ import android.os.PersistableBundle; import com.android.internal.util.HexDump; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -296,6 +298,30 @@ public class PersistableBundleUtils { } /** + * Converts a PersistableBundle into a disk-stable byte array format + * + * @param bundle the PersistableBundle to be converted to a disk-stable format + * @return the byte array representation of the PersistableBundle + */ + @Nullable + public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bundle.writeToStream(outputStream); + return outputStream.toByteArray(); + } + + /** + * Converts from a disk-stable byte array format to a PersistableBundle + * + * @param bytes the disk-stable byte array + * @return the PersistableBundle parsed from this byte array. + */ + public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + return PersistableBundle.readFromStream(inputStream); + } + + /** * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk. * * <p>This class will enforce exclusion between reads and writes using the standard semantics of diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java index 34483957ca12..48e6f972fc8e 100644 --- a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java +++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java @@ -59,7 +59,6 @@ public abstract class ActivityInterceptorCallback { @IntDef(suffix = { "_ORDERED_ID" }, value = { FIRST_ORDERED_ID, PERMISSION_POLICY_ORDERED_ID, - INTENT_RESOLVER_ORDERED_ID, VIRTUAL_DEVICE_SERVICE_ORDERED_ID, DREAM_MANAGER_ORDERED_ID, LAST_ORDERED_ID // Update this when adding new ids @@ -78,11 +77,6 @@ public abstract class ActivityInterceptorCallback { public static final int PERMISSION_POLICY_ORDERED_ID = 1; /** - * The identifier for {@link com.android.server.pm.IntentResolverInterceptor}. - */ - public static final int INTENT_RESOLVER_ORDERED_ID = 2; - - /** * The identifier for {@link com.android.server.companion.virtual.VirtualDeviceManagerService} * interceptor. */ diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 6bb7acd65143..189fff865ea0 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -5095,13 +5095,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // still check DC#okToAnimate again if the transition animation is fine to apply. // TODO(new-app-transition): Rewrite this logic using WM Shell. final boolean recentsAnimating = isAnimating(PARENTS, ANIMATION_TYPE_RECENTS); + final boolean isEnteringPipWithoutVisibleChange = mWaitForEnteringPinnedMode + && mVisible == visible; if (okToAnimate(true /* ignoreFrozen */, canTurnScreenOn()) && (appTransition.isTransitionSet() || (recentsAnimating && !isActivityTypeHome())) - // If the visibility change during enter PIP, we don't want to include it in app - // transition to affect the animation theme, because the Pip organizer will animate - // the entering PIP instead. - && !mWaitForEnteringPinnedMode) { + // If the visibility is not changed during enter PIP, we don't want to include it in + // app transition to affect the animation theme, because the Pip organizer will + // animate the entering PIP instead. + && !isEnteringPipWithoutVisibleChange) { if (visible) { displayContent.mOpeningApps.add(this); mEnteringAnimation = true; @@ -6340,8 +6342,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mSharedStartingData != null ? mSharedStartingData.mAssociatedTask : null; if (associatedTask == null) { removeStartingWindow(); - } else if (associatedTask.getActivity( - r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) { + } else if (associatedTask.getActivity(r -> r.mVisibleRequested && !r.firstWindowDrawn + // Don't block starting window removal if an Activity can't be a starting window + // target. + && r.mSharedStartingData != null) == null) { // The last drawn activity may not be the one that owns the starting window. final ActivityRecord r = associatedTask.topActivityContainsStartingWindow(); if (r != null) { @@ -7984,20 +7988,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // orientation with insets applied. return; } - // Not using Task#isResizeable() or ActivityRecord#isResizeable() directly because app - // compatibility testing showed that android:supportsPictureInPicture="true" alone is not - // sufficient signal for not letterboxing an app. - // TODO(214602463): Remove multi-window check since orientation and aspect ratio - // restrictions should always be applied in multi-window. - final boolean isResizeable = task != null - // Activity should be resizable if the task is. - ? task.isResizeable(/* checkPictureInPictureSupport */ false) - || isResizeable(/* checkPictureInPictureSupport */ false) - : isResizeable(/* checkPictureInPictureSupport */ false); - if (WindowConfiguration.inMultiWindowMode(windowingMode) && isResizeable) { - // Ignore orientation request for resizable apps in multi window. - return; - } + if (windowingMode == WINDOWING_MODE_PINNED) { // PiP bounds have higher priority than the requested orientation. Otherwise the // activity may be squeezed into a small piece. diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 72408b67de41..d60981fcf504 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -518,6 +518,8 @@ public class ActivityStartController { .setRequestCode(-1) .setCallingUid(callingUid) .setCallingPid(callingPid) + .setRealCallingUid(callingUid) + .setRealCallingPid(callingPid) .setUserId(caller != null ? caller.mUserId : mService.getCurrentUserId()) .execute(); } diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index cb6559715c55..fa506ca68e57 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -663,11 +663,16 @@ public class AppTransitionController { "Override with TaskFragment remote animation for transit=%s", AppTransition.appTransitionOldToString(transit)); + final int organizerUid = mDisplayContent.mAtmService.mTaskFragmentOrganizerController + .getTaskFragmentOrganizerUid(organizer); + final boolean shouldDisableInputForRemoteAnimation = !task.isFullyTrustedEmbedding( + organizerUid); final RemoteAnimationController remoteAnimationController = mDisplayContent.mAppTransition.getRemoteAnimationController(); - if (remoteAnimationController != null) { + if (shouldDisableInputForRemoteAnimation && remoteAnimationController != null) { // We are going to use client-driven animation, Disable all input on activity windows - // during the animation to ensure it is safe to allow client to animate the surfaces. + // during the animation (unless it is fully trusted) to ensure it is safe to allow + // client to animate the surfaces. // This is needed for all activity windows in the animation Task. remoteAnimationController.setOnRemoteAnimationReady(() -> { final Consumer<ActivityRecord> updateActivities = @@ -894,7 +899,11 @@ public class AppTransitionController { // We cannot promote the animation on Task's parent when the task is in // clearing task in case the animating get stuck when performing the opening // task that behind it. - || (current.asTask() != null && current.asTask().mInRemoveTask)) { + || (current.asTask() != null && current.asTask().mInRemoveTask) + // We cannot promote the animation to changing window. This may happen when an + // activity is open in a TaskFragment that is resizing, while the existing + // activity in the TaskFragment is reparented to another TaskFragment. + || parent.isChangingAppTransition()) { canPromote = false; } else { // In case a descendant of the parent belongs to the other group, we cannot promote diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java index 029056ac26fb..e7ab63eab202 100644 --- a/services/core/java/com/android/server/wm/Dimmer.java +++ b/services/core/java/com/android/server/wm/Dimmer.java @@ -50,11 +50,16 @@ class Dimmer { } @Override - public SurfaceControl.Transaction getPendingTransaction() { + public SurfaceControl.Transaction getSyncTransaction() { return mHost.getSyncTransaction(); } @Override + public SurfaceControl.Transaction getPendingTransaction() { + return mHost.getPendingTransaction(); + } + + @Override public void commitPendingTransaction() { mHost.commitPendingTransaction(); } @@ -105,7 +110,7 @@ class Dimmer { void removeSurface() { if (mDimLayer != null && mDimLayer.isValid()) { - getPendingTransaction().remove(mDimLayer); + getSyncTransaction().remove(mDimLayer); } mDimLayer = null; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index c12f7f33b069..66eca990b14f 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -6228,6 +6228,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** * Sets if Display APIs should be sandboxed to the activity window bounds. */ + @VisibleForTesting void setSandboxDisplayApis(boolean sandboxDisplayApis) { mSandboxDisplayApis = sandboxDisplayApis; } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index d62bcdfd446b..442f70846118 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1190,7 +1190,8 @@ public class DisplayPolicy { if (attrs.providesInsetsTypes != null) { for (@InternalInsetsType int insetsType : attrs.providesInsetsTypes) { final TriConsumer<DisplayFrames, WindowContainer, Rect> imeFrameProvider = - (displayFrames, windowContainer, inOutFrame) -> { + win.getAttrs().providedInternalImeInsets != null + ? (displayFrames, windowContainer, inOutFrame) -> { final Insets[] providedInternalImeInsets = win.getLayoutingAttrs(displayFrames.mRotation) .providedInternalImeInsets; @@ -1199,7 +1200,7 @@ public class DisplayPolicy { && providedInternalImeInsets[insetsType] != null) { inOutFrame.inset(providedInternalImeInsets[insetsType]); } - }; + } : null; switch (insetsType) { case ITYPE_STATUS_BAR: mStatusBarAlt = win; @@ -1218,17 +1219,18 @@ public class DisplayPolicy { mExtraNavBarAltPosition = getAltBarPosition(attrs); break; } - mDisplayContent.setInsetProvider(insetsType, win, (displayFrames, - windowContainer, inOutFrame) -> { - final Insets[] providedInternalInsets = win.getLayoutingAttrs( - displayFrames.mRotation).providedInternalInsets; - if (providedInternalInsets != null - && providedInternalInsets.length > insetsType - && providedInternalInsets[insetsType] != null) { - inOutFrame.inset(providedInternalInsets[insetsType]); - } - inOutFrame.inset(win.mGivenContentInsets); - }, imeFrameProvider); + mDisplayContent.setInsetProvider(insetsType, win, + win.getAttrs().providedInternalInsets != null ? (displayFrames, + windowContainer, inOutFrame) -> { + final Insets[] providedInternalInsets = win.getLayoutingAttrs( + displayFrames.mRotation).providedInternalInsets; + if (providedInternalInsets != null + && providedInternalInsets.length > insetsType + && providedInternalInsets[insetsType] != null) { + inOutFrame.inset(providedInternalInsets[insetsType]); + } + inOutFrame.inset(win.mGivenContentInsets); + } : null, imeFrameProvider); mInsetsSourceWindowsExceptIme.add(win); } } @@ -2407,7 +2409,9 @@ public class DisplayPolicy { private int updateSystemBarsLw(WindowState win, int disableFlags) { final TaskDisplayArea defaultTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea(); final boolean multiWindowTaskVisible = - defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_MULTI_WINDOW); + defaultTaskDisplayArea.getRootTask(task -> task.isVisible() + && task.getTopLeafTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) + != null; final boolean freeformRootTaskVisible = defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_FREEFORM); diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index ea72e12783c3..ce416ad89c95 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -416,8 +416,22 @@ final class InputMonitor { final IBinder focusToken = focus != null ? focus.mInputChannelToken : null; if (focusToken == null) { + if (recentsAnimationInputConsumer != null + && recentsAnimationInputConsumer.mWindowHandle != null + && mInputFocus == recentsAnimationInputConsumer.mWindowHandle.token) { + // Avoid removing input focus from recentsAnimationInputConsumer. + // When the recents animation input consumer has the input focus, + // mInputFocus does not match to mDisplayContent.mCurrentFocus. Making it to be + // a special case, that do not remove the input focus from it when + // mDisplayContent.mCurrentFocus is null. This special case should be removed + // once recentAnimationInputConsumer is removed. + return; + } // When an app is focused, but its window is not showing yet, remove the input focus - // from the current window. + // from the current window. This enforces the input focus to match + // mDisplayContent.mCurrentFocus. However, if more special cases are discovered that + // the input focus and mDisplayContent.mCurrentFocus are expected to mismatch, + // the whole logic of how and when to revoke focus needs to be checked. if (mDisplayContent.mFocusedApp != null && mInputFocus != null) { ProtoLog.v(WM_DEBUG_FOCUS_LIGHT, "App %s is focused," + " but the window is not ready. Start a transaction to remove focus from" diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 9853d1304b14..358e93d89f64 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -82,6 +82,7 @@ abstract class InsetsSourceProvider { private boolean mIsLeashReadyForDispatching; private final Rect mSourceFrame = new Rect(); private final Rect mLastSourceFrame = new Rect(); + private @NonNull Insets mInsetsHint = Insets.NONE; private final Consumer<Transaction> mSetLeashPositionConsumer = t -> { if (mControl != null) { @@ -298,6 +299,7 @@ abstract class InsetsSourceProvider { if (!insetsHint.equals(mControl.getInsetsHint())) { changed = true; mControl.setInsetsHint(insetsHint); + mInsetsHint = insetsHint; } mLastSourceFrame.set(mSource.getFrame()); } @@ -433,8 +435,8 @@ abstract class InsetsSourceProvider { final SurfaceControl leash = mAdapter.mCapturedLeash; mControlTarget = target; updateVisibility(); - mControl = new InsetsSourceControl(mSource.getType(), leash, surfacePosition, - mSource.calculateInsets(mWindowContainer.getBounds(), true /* ignoreVisibility */)); + mControl = new InsetsSourceControl(mSource.getType(), leash, surfacePosition, mInsetsHint); + ProtoLog.d(WM_DEBUG_WINDOW_INSETS, "InsetsSource Control %s for target %s", mControl, mControlTarget); } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index ad2767c41e82..d02ad992c7e8 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -22,6 +22,7 @@ import android.content.Context; import android.graphics.Color; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -156,6 +157,7 @@ final class LetterboxConfiguration { * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and * the framework implementation will be used to determine the aspect ratio. */ + @VisibleForTesting void setFixedOrientationLetterboxAspectRatio(float aspectRatio) { mFixedOrientationLetterboxAspectRatio = aspectRatio; } @@ -164,6 +166,7 @@ final class LetterboxConfiguration { * Resets the aspect ratio of letterbox for fixed orientation to {@link * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio}. */ + @VisibleForTesting void resetFixedOrientationLetterboxAspectRatio() { mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat( com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio); @@ -177,25 +180,6 @@ final class LetterboxConfiguration { } /** - * Overrides corners raidus for activities presented in the letterbox mode. If given value < 0, - * both it and a value of {@link - * com.android.internal.R.integer.config_letterboxActivityCornersRadius} will be ignored and - * corners of the activity won't be rounded. - */ - void setLetterboxActivityCornersRadius(int cornersRadius) { - mLetterboxActivityCornersRadius = cornersRadius; - } - - /** - * Resets corners raidus for activities presented in the letterbox mode to {@link - * com.android.internal.R.integer.config_letterboxActivityCornersRadius}. - */ - void resetLetterboxActivityCornersRadius() { - mLetterboxActivityCornersRadius = mContext.getResources().getInteger( - com.android.internal.R.integer.config_letterboxActivityCornersRadius); - } - - /** * Whether corners of letterboxed activities are rounded. */ boolean isLetterboxActivityCornersRounded() { @@ -226,34 +210,6 @@ final class LetterboxConfiguration { return Color.valueOf(mContext.getResources().getColor(colorId)); } - - /** - * Sets color of letterbox background which is used when {@link - * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as - * fallback for other backfround types. - */ - void setLetterboxBackgroundColor(Color color) { - mLetterboxBackgroundColorOverride = color; - } - - /** - * Sets color ID of letterbox background which is used when {@link - * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as - * fallback for other backfround types. - */ - void setLetterboxBackgroundColorResourceId(int colorId) { - mLetterboxBackgroundColorResourceIdOverride = colorId; - } - - /** - * Resets color of letterbox background to {@link - * com.android.internal.R.color.config_letterboxBackgroundColor}. - */ - void resetLetterboxBackgroundColor() { - mLetterboxBackgroundColorOverride = null; - mLetterboxBackgroundColorResourceIdOverride = null; - } - /** * Gets {@link LetterboxBackgroundType} specified in {@link * com.android.internal.R.integer.config_letterboxBackgroundType} or over via ADB command. @@ -263,19 +219,6 @@ final class LetterboxConfiguration { return mLetterboxBackgroundType; } - /** Sets letterbox background type. */ - void setLetterboxBackgroundType(@LetterboxBackgroundType int backgroundType) { - mLetterboxBackgroundType = backgroundType; - } - - /** - * Resets cletterbox background type to {@link - * com.android.internal.R.integer.config_letterboxBackgroundType}. - */ - void resetLetterboxBackgroundType() { - mLetterboxBackgroundType = readLetterboxBackgroundTypeFromConfig(mContext); - } - /** Returns a string representing the given {@link LetterboxBackgroundType}. */ static String letterboxBackgroundTypeToString( @LetterboxBackgroundType int backgroundType) { @@ -305,27 +248,6 @@ final class LetterboxConfiguration { } /** - * Overrides alpha of a black scrim shown over wallpaper for {@link - * #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link mLetterboxBackgroundType}. - * - * <p>If given value is < 0 or >= 1, both it and a value of {@link - * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha} are ignored - * and 0.0 (transparent) is instead. - */ - void setLetterboxBackgroundWallpaperDarkScrimAlpha(float alpha) { - mLetterboxBackgroundWallpaperDarkScrimAlpha = alpha; - } - - /** - * Resets alpha of a black scrim shown over wallpaper letterbox background to {@link - * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha}. - */ - void resetLetterboxBackgroundWallpaperDarkScrimAlpha() { - mLetterboxBackgroundWallpaperDarkScrimAlpha = mContext.getResources().getFloat( - com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha); - } - - /** * Gets alpha of a black scrim shown over wallpaper letterbox background. */ float getLetterboxBackgroundWallpaperDarkScrimAlpha() { @@ -333,28 +255,6 @@ final class LetterboxConfiguration { } /** - * Overrides blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in - * {@link mLetterboxBackgroundType}. - * - * <p> If given value <= 0, both it and a value of {@link - * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius} are ignored - * and 0 is used instead. - */ - void setLetterboxBackgroundWallpaperBlurRadius(int radius) { - mLetterboxBackgroundWallpaperBlurRadius = radius; - } - - /** - * Resets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link - * mLetterboxBackgroundType} to {@link - * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius}. - */ - void resetLetterboxBackgroundWallpaperBlurRadius() { - mLetterboxBackgroundWallpaperBlurRadius = mContext.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius); - } - - /** * Gets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link * mLetterboxBackgroundType}. */ @@ -381,6 +281,7 @@ final class LetterboxConfiguration { * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier} are ignored and * central position (0.5) is used. */ + @VisibleForTesting void setLetterboxHorizontalPositionMultiplier(float multiplier) { mLetterboxHorizontalPositionMultiplier = multiplier; } @@ -389,6 +290,7 @@ final class LetterboxConfiguration { * Resets horizontal position of a center of the letterboxed app window to {@link * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier}. */ + @VisibleForTesting void resetLetterboxHorizontalPositionMultiplier() { mLetterboxHorizontalPositionMultiplier = mContext.getResources().getFloat( com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier); @@ -406,6 +308,7 @@ final class LetterboxConfiguration { * Overrides whether reachability repositioning is allowed for letterboxed fullscreen apps in * landscape device orientation. */ + @VisibleForTesting void setIsReachabilityEnabled(boolean enabled) { mIsReachabilityEnabled = enabled; } @@ -414,6 +317,7 @@ final class LetterboxConfiguration { * Resets whether reachability repositioning is allowed for letterboxed fullscreen apps in * landscape device orientation to {@link R.bool.config_letterboxIsReachabilityEnabled}. */ + @VisibleForTesting void resetIsReachabilityEnabled() { mIsReachabilityEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsReachabilityEnabled); @@ -429,22 +333,6 @@ final class LetterboxConfiguration { return mDefaultPositionForReachability; } - /** - * Overrides default horizonal position of the letterboxed app window when reachability - * is enabled. - */ - void setDefaultPositionForReachability(@LetterboxReachabilityPosition int position) { - mDefaultPositionForReachability = position; - } - - /** - * Resets default horizontal position of the letterboxed app window when reachability is - * enabled to {@link R.integer.config_letterboxDefaultPositionForReachability}. - */ - void resetDefaultPositionForReachability() { - mDefaultPositionForReachability = readLetterboxReachabilityPositionFromConfig(mContext); - } - @LetterboxReachabilityPosition private static int readLetterboxReachabilityPositionFromConfig(Context context) { int position = context.getResources().getInteger( @@ -516,17 +404,8 @@ final class LetterboxConfiguration { /** * Overrides whether education is allowed for letterboxed fullscreen apps. */ + @VisibleForTesting void setIsEducationEnabled(boolean enabled) { mIsEducationEnabled = enabled; } - - /** - * Resets whether education is allowed for letterboxed fullscreen apps to - * {@link R.bool.config_letterboxIsEducationEnabled}. - */ - void resetIsEducationEnabled() { - mIsEducationEnabled = mContext.getResources().getBoolean( - R.bool.config_letterboxIsEducationEnabled); - } - } diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java index 160fc95f3f7c..7a055d2948ad 100644 --- a/services/core/java/com/android/server/wm/LockTaskController.java +++ b/services/core/java/com/android/server/wm/LockTaskController.java @@ -64,6 +64,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.telephony.CellBroadcastUtils; import com.android.internal.widget.LockPatternUtils; import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; @@ -392,6 +393,10 @@ public class LockTaskController { return false; } + if (isWirelessEmergencyAlert(intent)) { + return false; + } + return !(isTaskAuthAllowlisted(taskAuth) || mLockTaskModeTasks.isEmpty()); } @@ -424,6 +429,25 @@ public class LockTaskController { return isPackageAllowlisted(userId, packageName); } + private boolean isWirelessEmergencyAlert(Intent intent) { + if (intent == null) { + return false; + } + + final ComponentName cellBroadcastAlertDialogComponentName = + CellBroadcastUtils.getDefaultCellBroadcastAlertDialogComponent(mContext); + + if (cellBroadcastAlertDialogComponentName == null) { + return false; + } + + if (cellBroadcastAlertDialogComponentName.equals(intent.getComponent())) { + return true; + } + + return false; + } + private boolean isEmergencyCallIntent(Intent intent) { if (intent == null) { return false; diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java index 2bae59a93048..b61af2f9febe 100644 --- a/services/core/java/com/android/server/wm/RecentsAnimationController.java +++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java @@ -1197,7 +1197,10 @@ public class RecentsAnimationController implements DeathRecipient { * this is the target task, CLOSING otherwise). */ RemoteAnimationTarget createRemoteAnimationTarget(int overrideTaskId, int overrideMode) { - final ActivityRecord topApp = mTask.getTopVisibleActivity(); + ActivityRecord topApp = mTask.getTopRealVisibleActivity(); + if (topApp == null) { + topApp = mTask.getTopVisibleActivity(); + } final WindowState mainWindow = topApp != null ? topApp.findMainWindow() : null; diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index b4029d185b9f..518bfd4c90df 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -565,6 +565,7 @@ class ScreenRotationAnimation { private SimpleSurfaceAnimatable.Builder initializeBuilder() { return new SimpleSurfaceAnimatable.Builder() + .setSyncTransactionSupplier(mDisplayContent::getSyncTransaction) .setPendingTransactionSupplier(mDisplayContent::getPendingTransaction) .setCommitTransactionRunnable(mDisplayContent::commitPendingTransaction) .setAnimationLeashSupplier(mDisplayContent::makeOverlay); diff --git a/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java b/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java index bf5d5e2653a8..3b3db890f67e 100644 --- a/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java +++ b/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java @@ -41,6 +41,7 @@ public class SimpleSurfaceAnimatable implements SurfaceAnimator.Animatable { private final SurfaceControl mParentSurfaceControl; private final Runnable mCommitTransactionRunnable; private final Supplier<SurfaceControl.Builder> mAnimationLeashFactory; + private final Supplier<SurfaceControl.Transaction> mSyncTransaction; private final Supplier<SurfaceControl.Transaction> mPendingTransaction; private final BiConsumer<SurfaceControl.Transaction, SurfaceControl> mOnAnimationLeashCreated; private final Consumer<SurfaceControl.Transaction> mOnAnimationLeashLost; @@ -60,10 +61,16 @@ public class SimpleSurfaceAnimatable implements SurfaceAnimator.Animatable { mAnimationLeashFactory = builder.mAnimationLeashFactory; mOnAnimationLeashCreated = builder.mOnAnimationLeashCreated; mOnAnimationLeashLost = builder.mOnAnimationLeashLost; + mSyncTransaction = builder.mSyncTransactionSupplier; mPendingTransaction = builder.mPendingTransactionSupplier; mOnAnimationFinished = builder.mOnAnimationFinished; } + @Override + public SurfaceControl.Transaction getSyncTransaction() { + return mSyncTransaction.get(); + } + @NonNull @Override public SurfaceControl.Transaction getPendingTransaction() { @@ -160,6 +167,9 @@ public class SimpleSurfaceAnimatable implements SurfaceAnimator.Animatable { private Consumer<Runnable> mOnAnimationFinished = null; @NonNull + private Supplier<SurfaceControl.Transaction> mSyncTransactionSupplier; + + @NonNull private Supplier<SurfaceControl.Transaction> mPendingTransactionSupplier; @NonNull @@ -207,6 +217,15 @@ public class SimpleSurfaceAnimatable implements SurfaceAnimator.Animatable { } /** + * @see SurfaceAnimator.Animatable#getSyncTransaction() + */ + public Builder setSyncTransactionSupplier( + @NonNull Supplier<SurfaceControl.Transaction> syncTransactionSupplier) { + mSyncTransactionSupplier = syncTransactionSupplier; + return this; + } + + /** * @see SurfaceAnimator.Animatable#getPendingTransaction() */ public Builder setPendingTransactionSupplier( @@ -290,6 +309,9 @@ public class SimpleSurfaceAnimatable implements SurfaceAnimator.Animatable { } public SurfaceAnimator.Animatable build() { + if (mSyncTransactionSupplier == null) { + throw new IllegalArgumentException("mSyncTransactionSupplier cannot be null"); + } if (mPendingTransactionSupplier == null) { throw new IllegalArgumentException("mPendingTransactionSupplier cannot be null"); } diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java index fbf04262cc37..3dde2f1aa01f 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimator.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java @@ -128,7 +128,7 @@ class SurfaceAnimator { } final OnAnimationFinishedCallback animationFinishCallback = mSurfaceAnimationFinishedCallback; - reset(mAnimatable.getPendingTransaction(), true /* destroyLeash */); + reset(mAnimatable.getSyncTransaction(), true /* destroyLeash */); if (staticAnimationFinishedCallback != null) { staticAnimationFinishedCallback.onAnimationFinished(type, anim); } @@ -234,7 +234,7 @@ class SurfaceAnimator { final boolean delayed = mAnimationStartDelayed; mAnimationStartDelayed = false; if (delayed && mAnimation != null) { - mAnimation.startAnimation(mLeash, mAnimatable.getPendingTransaction(), + mAnimation.startAnimation(mLeash, mAnimatable.getSyncTransaction(), mAnimationType, mInnerAnimationFinishedCallback); mAnimatable.commitPendingTransaction(); } @@ -264,7 +264,7 @@ class SurfaceAnimator { * Cancels any currently running animation. */ void cancelAnimation() { - cancelAnimation(mAnimatable.getPendingTransaction(), false /* restarting */, + cancelAnimation(mAnimatable.getSyncTransaction(), false /* restarting */, true /* forwardCancel */); mAnimatable.commitPendingTransaction(); } @@ -319,7 +319,7 @@ class SurfaceAnimator { return; } endDelayingAnimationStart(); - final Transaction t = mAnimatable.getPendingTransaction(); + final Transaction t = mAnimatable.getSyncTransaction(); cancelAnimation(t, true /* restarting */, true /* forwardCancel */); mLeash = from.mLeash; mAnimation = from.mAnimation; @@ -620,6 +620,12 @@ class SurfaceAnimator { interface Animatable { /** + * Use this method instead of {@link #getPendingTransaction()} if the transaction should be + * synchronized with the client. + */ + @NonNull Transaction getSyncTransaction(); + + /** * @return The pending transaction that will be committed in the next frame. */ @NonNull Transaction getPendingTransaction(); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 60c280cb61f9..7ad53f96a80e 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3070,11 +3070,22 @@ class Task extends TaskFragment { }); } + /** + * Return the top visible requested activity. The activity has been requested to be visible, + * but it's possible that the activity has just been created, so no window is yet attached to + * this activity. + */ ActivityRecord getTopVisibleActivity() { - return getActivity((r) -> { - // skip hidden (or about to hide) apps - return !r.mIsExiting && r.isClientVisible() && r.mVisibleRequested; - }); + return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.mVisibleRequested); + } + + /** + * Return the top visible activity. The activity has a window on which contents are drawn. + * However it's possible that the activity has already been requested to be invisible, but the + * visibility is not yet committed. + */ + ActivityRecord getTopRealVisibleActivity() { + return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.isVisible()); } ActivityRecord getTopWaitSplashScreenActivity() { diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index ce406e4ecb20..dd1b50fc5e0b 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -654,12 +654,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { } // Apps and their containers are not allowed to specify an orientation of non floating - // visible tasks created by organizer. The organizer handles the orientation instead. + // visible tasks created by organizer and that has an adjacent task. final Task nonFloatingTopTask = - getRootTask(t -> !t.getWindowConfiguration().tasksAreFloating()); - if (nonFloatingTopTask != null && nonFloatingTopTask.mCreatedByOrganizer - && nonFloatingTopTask.isVisible()) { - return SCREEN_ORIENTATION_UNSPECIFIED; + getTask(t -> !t.getWindowConfiguration().tasksAreFloating()); + if (nonFloatingTopTask != null) { + final Task task = nonFloatingTopTask.getCreatedByOrganizerTask(); + if (task != null && task.getAdjacentTaskFragment() != null && task.isVisible()) { + return SCREEN_ORIENTATION_UNSPECIFIED; + } } final int orientation = super.getOrientation(candidate); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 56e96fa1fe58..22df8b06e398 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -562,13 +562,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { * @param uid uid of the TaskFragment organizer. */ boolean isAllowedToEmbedActivityInTrustedMode(@NonNull ActivityRecord a, int uid) { - if (UserHandle.getAppId(uid) == SYSTEM_UID) { - // The system is trusted to embed other apps securely and for all users. - return true; - } - - if (uid == a.getUid()) { - // Activities from the same UID can be embedded freely by the host. + if (isFullyTrustedEmbedding(a, uid)) { return true; } @@ -587,13 +581,34 @@ class TaskFragment extends WindowContainer<WindowContainer> { } /** + * It is fully trusted for embedding in the system app or embedding in the same app. This is + * different from {@link #isAllowedToBeEmbeddedInTrustedMode()} since there may be a small + * chance for a previous trusted app to start doing something bad. + */ + private static boolean isFullyTrustedEmbedding(@NonNull ActivityRecord a, int uid) { + // The system is trusted to embed other apps securely and for all users. + return UserHandle.getAppId(uid) == SYSTEM_UID + // Activities from the same UID can be embedded freely by the host. + || uid == a.getUid(); + } + + /** + * Checks if all activities in the task fragment are embedded as fully trusted. + * @see #isFullyTrustedEmbedding(ActivityRecord, int) + * @param uid uid of the TaskFragment organizer. + */ + boolean isFullyTrustedEmbedding(int uid) { + // Traverse all activities to see if any of them are not fully trusted embedding. + return !forAllActivities(r -> !isFullyTrustedEmbedding(r, uid)); + } + + /** * Checks if all activities in the task fragment are allowed to be embedded in trusted mode. * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord) */ boolean isAllowedToBeEmbeddedInTrustedMode() { // Traverse all activities to see if any of them are not in the trusted mode. - final Predicate<ActivityRecord> callback = r -> !isAllowedToEmbedActivityInTrustedMode(r); - return !forAllActivities(callback); + return !forAllActivities(r -> !isAllowedToEmbedActivityInTrustedMode(r)); } /** diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 47e606a5313e..b4d1cf77919a 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -378,6 +378,11 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } + int getTaskFragmentOrganizerUid(ITaskFragmentOrganizer organizer) { + final TaskFragmentOrganizerState state = validateAndGetState(organizer); + return state.mOrganizerUid; + } + void onTaskFragmentAppeared(ITaskFragmentOrganizer organizer, TaskFragment taskFragment) { final TaskFragmentOrganizerState state = validateAndGetState(organizer); if (!state.addTaskFragment(taskFragment)) { diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index a480c37fbcf3..f9d19e22ddc7 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1004,7 +1004,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< void onDisplayChanged(DisplayContent dc) { if (mDisplayContent != null && mDisplayContent.mChangingContainers.remove(this)) { // Cancel any change transition queued-up for this container on the old display. - mSurfaceFreezer.unfreeze(getPendingTransaction()); + mSurfaceFreezer.unfreeze(getSyncTransaction()); } mDisplayContent = dc; if (dc != null && dc != this) { @@ -2697,6 +2697,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * @return {@link #mBLASTSyncTransaction} if available. Otherwise, returns * {@link #getPendingTransaction()} */ + @Override public Transaction getSyncTransaction() { if (mSyncTransactionCommitCallbackDepth > 0) { return mSyncTransaction; @@ -2767,7 +2768,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< void cancelAnimation() { doAnimationFinished(mSurfaceAnimator.getAnimationType(), mSurfaceAnimator.getAnimation()); mSurfaceAnimator.cancelAnimation(); - mSurfaceFreezer.unfreeze(getPendingTransaction()); + mSurfaceFreezer.unfreeze(getSyncTransaction()); } /** Whether we can start change transition with this window and current display status. */ diff --git a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java index 7f21eeb43d59..9b6f4d947694 100644 --- a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java +++ b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java @@ -167,6 +167,11 @@ class WindowContainerThumbnail implements Animatable { } @Override + public Transaction getSyncTransaction() { + return mWindowContainer.getSyncTransaction(); + } + + @Override public Transaction getPendingTransaction() { return mWindowContainer.getPendingTransaction(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9872f55ad383..7a5480401de8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -5704,25 +5704,6 @@ public class WindowManagerService extends IWindowManager.Stub } } - void setSandboxDisplayApis(int displayId, boolean sandboxDisplayApis) { - if (mContext.checkCallingOrSelfPermission(WRITE_SECURE_SETTINGS) - != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Must hold permission " + WRITE_SECURE_SETTINGS); - } - - final long ident = Binder.clearCallingIdentity(); - try { - synchronized (mGlobalLock) { - final DisplayContent displayContent = mRoot.getDisplayContent(displayId); - if (displayContent != null) { - displayContent.setSandboxDisplayApis(sandboxDisplayApis); - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - /** The global settings only apply to default display. */ private boolean applyForcedPropertiesForDefaultDisplay() { boolean changed = false; diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 5a2f28f4a365..34c93482ecfe 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -19,16 +19,6 @@ package com.android.server.wm; import static android.os.Build.IS_USER; import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_CENTER; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_LEFT; -import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_RIGHT; - -import android.content.res.Resources.NotFoundException; -import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.os.ParcelFileDescriptor; @@ -46,8 +36,6 @@ import com.android.internal.os.ByteTransferPipe; import com.android.internal.protolog.ProtoLogImpl; import com.android.server.LocalServices; import com.android.server.statusbar.StatusBarManagerInternal; -import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType; -import com.android.server.wm.LetterboxConfiguration.LetterboxReachabilityPosition; import java.io.IOException; import java.io.PrintWriter; @@ -70,12 +58,10 @@ public class WindowManagerShellCommand extends ShellCommand { // Internal service impl -- must perform security checks before touching. private final WindowManagerService mInternal; - private final LetterboxConfiguration mLetterboxConfiguration; public WindowManagerShellCommand(WindowManagerService service) { mInterface = service; mInternal = service; - mLetterboxConfiguration = service.mLetterboxConfiguration; } @Override @@ -127,14 +113,6 @@ public class WindowManagerShellCommand extends ShellCommand { return runGetIgnoreOrientationRequest(pw); case "dump-visible-window-views": return runDumpVisibleWindowViews(pw); - case "set-letterbox-style": - return runSetLetterboxStyle(pw); - case "get-letterbox-style": - return runGetLetterboxStyle(pw); - case "reset-letterbox-style": - return runResetLetterboxStyle(pw); - case "set-sandbox-display-apis": - return runSandboxDisplayApis(pw); case "set-multi-window-config": return runSetMultiWindowConfig(); case "get-multi-window-config": @@ -353,37 +331,6 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } - /** - * Override display size and metrics to reflect the DisplayArea of the calling activity. - */ - private int runSandboxDisplayApis(PrintWriter pw) throws RemoteException { - int displayId = Display.DEFAULT_DISPLAY; - String arg = getNextArgRequired(); - if ("-d".equals(arg)) { - displayId = Integer.parseInt(getNextArgRequired()); - arg = getNextArgRequired(); - } - - final boolean sandboxDisplayApis; - switch (arg) { - case "true": - case "1": - sandboxDisplayApis = true; - break; - case "false": - case "0": - sandboxDisplayApis = false; - break; - default: - getErrPrintWriter().println("Error: expecting true, 1, false, 0, but we " - + "get " + arg); - return -1; - } - - mInternal.setSandboxDisplayApis(displayId, sandboxDisplayApis); - return 0; - } - private int runDismissKeyguard(PrintWriter pw) throws RemoteException { mInterface.dismissKeyguard(null /* callback */, null /* message */); return 0; @@ -606,347 +553,6 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } - private int runSetFixedOrientationLetterboxAspectRatio(PrintWriter pw) throws RemoteException { - final float aspectRatio; - try { - String arg = getNextArgRequired(); - aspectRatio = Float.parseFloat(arg); - } catch (NumberFormatException e) { - getErrPrintWriter().println("Error: bad aspect ratio format " + e); - return -1; - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: aspect ratio should be provided as an argument " + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio); - } - return 0; - } - - private int runSetLetterboxActivityCornersRadius(PrintWriter pw) throws RemoteException { - final int cornersRadius; - try { - String arg = getNextArgRequired(); - cornersRadius = Integer.parseInt(arg); - } catch (NumberFormatException e) { - getErrPrintWriter().println("Error: bad corners radius format " + e); - return -1; - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: corners radius should be provided as an argument " + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxActivityCornersRadius(cornersRadius); - } - return 0; - } - - private int runSetLetterboxBackgroundType(PrintWriter pw) throws RemoteException { - @LetterboxBackgroundType final int backgroundType; - try { - String arg = getNextArgRequired(); - switch (arg) { - case "solid_color": - backgroundType = LETTERBOX_BACKGROUND_SOLID_COLOR; - break; - case "app_color_background": - backgroundType = LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND; - break; - case "app_color_background_floating": - backgroundType = LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING; - break; - case "wallpaper": - backgroundType = LETTERBOX_BACKGROUND_WALLPAPER; - break; - default: - getErrPrintWriter().println( - "Error: 'solid_color', 'app_color_background' or " - + "'wallpaper' should be provided as an argument"); - return -1; - } - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: 'solid_color', 'app_color_background' or " - + "'wallpaper' should be provided as an argument" + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundType(backgroundType); - } - return 0; - } - - private int runSetLetterboxBackgroundColorResource(PrintWriter pw) throws RemoteException { - final int colorId; - try { - String arg = getNextArgRequired(); - colorId = mInternal.mContext.getResources() - .getIdentifier(arg, "color", "com.android.internal"); - } catch (NotFoundException e) { - getErrPrintWriter().println( - "Error: color in '@android:color/resource_name' format should be provided as " - + "an argument " + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundColorResourceId(colorId); - } - return 0; - } - - private int runSetLetterboxBackgroundColor(PrintWriter pw) throws RemoteException { - final Color color; - try { - String arg = getNextArgRequired(); - color = Color.valueOf(Color.parseColor(arg)); - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: color in #RRGGBB format should be provided as " - + "an argument " + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundColor(color); - } - return 0; - } - - private int runSetLetterboxBackgroundWallpaperBlurRadius(PrintWriter pw) - throws RemoteException { - final int radius; - try { - String arg = getNextArgRequired(); - radius = Integer.parseInt(arg); - } catch (NumberFormatException e) { - getErrPrintWriter().println("Error: blur radius format " + e); - return -1; - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: blur radius should be provided as an argument " + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundWallpaperBlurRadius(radius); - } - return 0; - } - - private int runSetLetterboxBackgroundWallpaperDarkScrimAlpha(PrintWriter pw) - throws RemoteException { - final float alpha; - try { - String arg = getNextArgRequired(); - alpha = Float.parseFloat(arg); - } catch (NumberFormatException e) { - getErrPrintWriter().println("Error: bad alpha format " + e); - return -1; - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: alpha should be provided as an argument " + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha); - } - return 0; - } - - private int runSetLetterboxHorizontalPositionMultiplier(PrintWriter pw) throws RemoteException { - final float multiplier; - try { - String arg = getNextArgRequired(); - multiplier = Float.parseFloat(arg); - } catch (NumberFormatException e) { - getErrPrintWriter().println("Error: bad multiplier format " + e); - return -1; - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: multiplier should be provided as an argument " + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier); - } - return 0; - } - - private int runSetLetterboxIsReachabilityEnabled(PrintWriter pw) throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsReachabilityEnabled(enabled); - } - return 0; - } - - private int runSetLetterboxDefaultPositionForReachability(PrintWriter pw) - throws RemoteException { - @LetterboxReachabilityPosition final int position; - try { - String arg = getNextArgRequired(); - switch (arg) { - case "left": - position = LETTERBOX_REACHABILITY_POSITION_LEFT; - break; - case "center": - position = LETTERBOX_REACHABILITY_POSITION_CENTER; - break; - case "right": - position = LETTERBOX_REACHABILITY_POSITION_RIGHT; - break; - default: - getErrPrintWriter().println( - "Error: 'left', 'center' or 'right' are expected as an argument"); - return -1; - } - } catch (IllegalArgumentException e) { - getErrPrintWriter().println( - "Error: 'left', 'center' or 'right' are expected as an argument" + e); - return -1; - } - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setDefaultPositionForReachability(position); - } - return 0; - } - - private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException { - String arg = getNextArg(); - final boolean enabled; - switch (arg) { - case "true": - case "1": - enabled = true; - break; - case "false": - case "0": - enabled = false; - break; - default: - getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); - return -1; - } - - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.setIsEducationEnabled(enabled); - } - return 0; - } - - private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException { - if (peekNextArg() == null) { - getErrPrintWriter().println("Error: No arguments provided."); - } - while (peekNextArg() != null) { - String arg = getNextArg(); - switch (arg) { - case "--aspectRatio": - runSetFixedOrientationLetterboxAspectRatio(pw); - break; - case "--cornerRadius": - runSetLetterboxActivityCornersRadius(pw); - break; - case "--backgroundType": - runSetLetterboxBackgroundType(pw); - break; - case "--backgroundColor": - runSetLetterboxBackgroundColor(pw); - break; - case "--backgroundColorResource": - runSetLetterboxBackgroundColorResource(pw); - break; - case "--wallpaperBlurRadius": - runSetLetterboxBackgroundWallpaperBlurRadius(pw); - break; - case "--wallpaperDarkScrimAlpha": - runSetLetterboxBackgroundWallpaperDarkScrimAlpha(pw); - break; - case "--horizontalPositionMultiplier": - runSetLetterboxHorizontalPositionMultiplier(pw); - break; - case "--isReachabilityEnabled": - runSetLetterboxIsReachabilityEnabled(pw); - break; - case "--defaultPositionForReachability": - runSetLetterboxDefaultPositionForReachability(pw); - break; - case "--isEducationEnabled": - runSetLetterboxIsEducationEnabled(pw); - break; - default: - getErrPrintWriter().println( - "Error: Unrecognized letterbox style option: " + arg); - return -1; - } - } - return 0; - } - - private int runResetLetterboxStyle(PrintWriter pw) throws RemoteException { - if (peekNextArg() == null) { - resetLetterboxStyle(); - } - synchronized (mInternal.mGlobalLock) { - while (peekNextArg() != null) { - String arg = getNextArg(); - switch (arg) { - case "aspectRatio": - mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio(); - break; - case "cornerRadius": - mLetterboxConfiguration.resetLetterboxActivityCornersRadius(); - break; - case "backgroundType": - mLetterboxConfiguration.resetLetterboxBackgroundType(); - break; - case "backgroundColor": - mLetterboxConfiguration.resetLetterboxBackgroundColor(); - break; - case "wallpaperBlurRadius": - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadius(); - break; - case "wallpaperDarkScrimAlpha": - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha(); - break; - case "horizontalPositionMultiplier": - mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier(); - break; - case "isReachabilityEnabled": - mLetterboxConfiguration.getIsReachabilityEnabled(); - break; - case "defaultPositionForReachability": - mLetterboxConfiguration.getDefaultPositionForReachability(); - break; - case "isEducationEnabled": - mLetterboxConfiguration.getIsEducationEnabled(); - break; - default: - getErrPrintWriter().println( - "Error: Unrecognized letterbox style option: " + arg); - return -1; - } - } - } - return 0; - } - private int runSetMultiWindowConfig() { if (peekNextArg() == null) { getErrPrintWriter().println("Error: No arguments provided."); @@ -1021,50 +627,6 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } - private void resetLetterboxStyle() { - synchronized (mInternal.mGlobalLock) { - mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio(); - mLetterboxConfiguration.resetLetterboxActivityCornersRadius(); - mLetterboxConfiguration.resetLetterboxBackgroundType(); - mLetterboxConfiguration.resetLetterboxBackgroundColor(); - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadius(); - mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha(); - mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier(); - mLetterboxConfiguration.resetIsReachabilityEnabled(); - mLetterboxConfiguration.resetDefaultPositionForReachability(); - mLetterboxConfiguration.resetIsEducationEnabled(); - } - } - - private int runGetLetterboxStyle(PrintWriter pw) throws RemoteException { - synchronized (mInternal.mGlobalLock) { - pw.println("Corner radius: " - + mLetterboxConfiguration.getLetterboxActivityCornersRadius()); - pw.println("Horizontal position multiplier: " - + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier()); - pw.println("Aspect ratio: " - + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio()); - pw.println("Is reachability enabled: " - + mLetterboxConfiguration.getIsReachabilityEnabled()); - pw.println("Default position for reachability: " - + LetterboxConfiguration.letterboxReachabilityPositionToString( - mLetterboxConfiguration.getDefaultPositionForReachability())); - pw.println("Is education enabled: " - + mLetterboxConfiguration.getIsEducationEnabled()); - - pw.println("Background type: " - + LetterboxConfiguration.letterboxBackgroundTypeToString( - mLetterboxConfiguration.getLetterboxBackgroundType())); - pw.println(" Background color: " + Integer.toHexString( - mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb())); - pw.println(" Wallpaper blur radius: " - + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius()); - pw.println(" Wallpaper dark scrim alpha: " - + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha()); - } - return 0; - } - private int runReset(PrintWriter pw) throws RemoteException { int displayId = getDisplayId(getNextArg()); @@ -1089,12 +651,6 @@ public class WindowManagerShellCommand extends ShellCommand { // set-ignore-orientation-request mInterface.setIgnoreOrientationRequest(displayId, false /* ignoreOrientationRequest */); - // set-letterbox-style - resetLetterboxStyle(); - - // set-sandbox-display-apis - mInternal.setSandboxDisplayApis(displayId, /* sandboxDisplayApis= */ true); - // set-multi-window-config runResetMultiWindowConfig(); @@ -1129,12 +685,7 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" set-ignore-orientation-request [-d DISPLAY_ID] [true|1|false|0]"); pw.println(" get-ignore-orientation-request [-d DISPLAY_ID] "); pw.println(" If app requested orientation should be ignored."); - pw.println(" set-sandbox-display-apis [true|1|false|0]"); - pw.println(" Sets override of Display APIs getRealSize / getRealMetrics to reflect "); - pw.println(" DisplayArea of the activity, or the window bounds if in letterbox or"); - pw.println(" Size Compat Mode."); - printLetterboxHelp(pw); printMultiWindowConfigHelp(pw); pw.println(" reset [-d DISPLAY_ID]"); @@ -1147,63 +698,6 @@ public class WindowManagerShellCommand extends ShellCommand { } } - private void printLetterboxHelp(PrintWriter pw) { - pw.println(" set-letterbox-style"); - pw.println(" Sets letterbox style using the following options:"); - pw.println(" --aspectRatio aspectRatio"); - pw.println(" Aspect ratio of letterbox for fixed orientation. If aspectRatio <= " - + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO); - pw.println(" both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will"); - pw.println(" be ignored and framework implementation will determine aspect ratio."); - pw.println(" --cornerRadius radius"); - pw.println(" Corners radius for activities in the letterbox mode. If radius < 0,"); - pw.println(" both it and R.integer.config_letterboxActivityCornersRadius will be"); - pw.println(" ignored and corners of the activity won't be rounded."); - pw.println(" --backgroundType [reset|solid_color|app_color_background"); - pw.println(" |app_color_background_floating|wallpaper]"); - pw.println(" Type of background used in the letterbox mode."); - pw.println(" --backgroundColor color"); - pw.println(" Color of letterbox which is be used when letterbox background type"); - pw.println(" is 'solid-color'. Use (set)get-letterbox-style to check and control"); - pw.println(" letterbox background type. See Color#parseColor for allowed color"); - pw.println(" formats (#RRGGBB and some colors by name, e.g. magenta or olive)."); - pw.println(" --backgroundColorResource resource_name"); - pw.println(" Color resource name of letterbox background which is used when"); - pw.println(" background type is 'solid-color'. Use (set)get-letterbox-style to"); - pw.println(" check and control background type. Parameter is a color resource"); - pw.println(" name, for example, @android:color/system_accent2_50."); - pw.println(" --wallpaperBlurRadius radius"); - pw.println(" Blur radius for 'wallpaper' letterbox background. If radius <= 0"); - pw.println(" both it and R.dimen.config_letterboxBackgroundWallpaperBlurRadius"); - pw.println(" are ignored and 0 is used."); - pw.println(" --wallpaperDarkScrimAlpha alpha"); - pw.println(" Alpha of a black translucent scrim shown over 'wallpaper'"); - pw.println(" letterbox background. If alpha < 0 or >= 1 both it and"); - pw.println(" R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha are ignored"); - pw.println(" and 0.0 (transparent) is used instead."); - pw.println(" --horizontalPositionMultiplier multiplier"); - pw.println(" Horizontal position of app window center. If multiplier < 0 or > 1,"); - pw.println(" both it and R.dimen.config_letterboxHorizontalPositionMultiplier"); - pw.println(" are ignored and central position (0.5) is used."); - pw.println(" --isReachabilityEnabled [true|1|false|0]"); - pw.println(" Whether reachability repositioning is allowed for letterboxed"); - pw.println(" fullscreen apps in landscape device orientation."); - pw.println(" --defaultPositionForReachability [left|center|right]"); - pw.println(" Default horizontal position of app window when reachability is."); - pw.println(" enabled."); - pw.println(" --isEducationEnabled [true|1|false|0]"); - pw.println(" Whether education is allowed for letterboxed fullscreen apps."); - pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType"); - pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha"); - pw.println(" |horizontalPositionMultiplier|isReachabilityEnabled"); - pw.println(" isEducationEnabled||defaultPositionMultiplierForReachability]"); - pw.println(" Resets overrides to default values for specified properties separated"); - pw.println(" by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'."); - pw.println(" If no arguments provided, all values will be reset."); - pw.println(" get-letterbox-style"); - pw.println(" Prints letterbox style configuration."); - } - private void printMultiWindowConfigHelp(PrintWriter pw) { pw.println(" set-multi-window-config"); pw.println(" Sets options to determine if activity should be shown in multi window:"); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 081ee2c82d8e..ff3b4a5bb44f 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -734,6 +734,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); break; } + if (parent.getTask() != activity.getTask()) { + final Throwable exception = new SecurityException("The reparented activity is" + + " not in the same Task as the target TaskFragment."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); + break; + } activity.reparent(parent, POSITION_TOP); effects |= TRANSACT_EFFECTS_LIFECYCLE; break; @@ -1542,6 +1548,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); return; } + if (newParentTF.getTask() != oldParent.getTask()) { + final Throwable exception = new SecurityException( + "The new parent is not in the same Task as the old parent."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception); + return; + } while (oldParent.hasChild()) { oldParent.getChildAt(0).reparent(newParentTF, POSITION_TOP); } diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp index 93152f2ea1b7..dbc1a00ce274 100644 --- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp +++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp @@ -66,13 +66,13 @@ using android::base::unique_fd; // Defines the maximum amount of VMAs we can send per process_madvise syscall. // Currently this is set to UIO_MAXIOV which is the maximum segments allowed by // iovec implementation used by process_madvise syscall -#define MAX_VMAS_PER_COMPACTION UIO_MAXIOV +#define MAX_VMAS_PER_BATCH UIO_MAXIOV // Maximum bytes that we can send per process_madvise syscall once this limit // is reached we split the remaining VMAs into another syscall. The MAX_RW_COUNT // limit is imposed by iovec implementation. However, if you want to use a smaller -// limit, it has to be a page aligned value, otherwise, compaction would fail. -#define MAX_BYTES_PER_COMPACTION MAX_RW_COUNT +// limit, it has to be a page aligned value. +#define MAX_BYTES_PER_BATCH MAX_RW_COUNT // Selected a high enough number to avoid clashing with linux errno codes #define ERROR_COMPACTION_CANCELLED -1000 @@ -83,6 +83,180 @@ namespace android { // before starting next VMA batch static std::atomic<bool> cancelRunningCompaction; +// A VmaBatch represents a set of VMAs that can be processed +// as VMAs are processed by client code it is expected that the +// VMAs get consumed which means they are discarded as they are +// processed so that the first element always is the next element +// to be sent +struct VmaBatch { + struct iovec* vmas; + // total amount of VMAs to reach the end of iovec + size_t totalVmas; + // total amount of bytes that are remaining within iovec + uint64_t totalBytes; +}; + +// Advances the iterator by the specified amount of bytes. +// This is used to remove already processed or no longer +// needed parts of the batch. +// Returns total bytes consumed +uint64_t consumeBytes(VmaBatch& batch, uint64_t bytesToConsume) { + if (CC_UNLIKELY(bytesToConsume) < 0) { + LOG(ERROR) << "Cannot consume negative bytes for VMA batch !"; + return 0; + } + + if (CC_UNLIKELY(bytesToConsume > batch.totalBytes)) { + // Avoid consuming more bytes than available + bytesToConsume = batch.totalBytes; + } + + uint64_t bytesConsumed = 0; + while (bytesConsumed < bytesToConsume) { + if (CC_UNLIKELY(batch.totalVmas > 0)) { + // No more vmas to consume + break; + } + if (CC_UNLIKELY(bytesConsumed + batch.vmas[0].iov_len > bytesToConsume)) { + // This vma can't be fully consumed, do it partially. + uint64_t bytesLeftToConsume = bytesToConsume - bytesConsumed; + bytesConsumed += bytesLeftToConsume; + batch.vmas[0].iov_base = (void*)((uint64_t)batch.vmas[0].iov_base + bytesLeftToConsume); + batch.vmas[0].iov_len -= bytesLeftToConsume; + batch.totalBytes -= bytesLeftToConsume; + return bytesConsumed; + } + // This vma can be fully consumed + bytesConsumed += batch.vmas[0].iov_len; + batch.totalBytes -= batch.vmas[0].iov_len; + --batch.totalVmas; + ++batch.vmas; + } + + return bytesConsumed; +} + +// given a source of vmas this class will act as a factory +// of VmaBatch objects and it will allow generating batches +// until there are no more left in the source vector. +// Note: the class does not actually modify the given +// vmas vector, instead it iterates on it until the end. +class VmaBatchCreator { + const std::vector<Vma>* sourceVmas; + // This is the destination array where batched VMAs will be stored + // it gets encapsulated into a VmaBatch which is the object + // meant to be used by client code. + struct iovec* destVmas; + + // Parameters to keep track of the iterator on the source vmas + int currentIndex_; + uint64_t currentOffset_; + +public: + VmaBatchCreator(const std::vector<Vma>* vmasToBatch, struct iovec* destVmasVec) + : sourceVmas(vmasToBatch), destVmas(destVmasVec), currentIndex_(0), currentOffset_(0) {} + + int currentIndex() { return currentIndex_; } + uint64_t currentOffset() { return currentOffset_; } + + // Generates a batch and moves the iterator on the source vmas + // past the last VMA in the batch. + // Returns true on success, false on failure + bool createNextBatch(VmaBatch& batch) { + if (currentIndex_ >= MAX_VMAS_PER_BATCH && currentIndex_ >= sourceVmas->size()) { + return false; + } + + const std::vector<Vma>& vmas = *sourceVmas; + batch.vmas = destVmas; + uint64_t totalBytesInBatch = 0; + int indexInBatch = 0; + + // Add VMAs to the batch up until we consumed all the VMAs or + // reached any imposed limit of VMAs per batch. + while (indexInBatch < MAX_VMAS_PER_BATCH && currentIndex_ < vmas.size()) { + uint64_t vmaStart = vmas[currentIndex_].start + currentOffset_; + uint64_t vmaSize = vmas[currentIndex_].end - vmaStart; + uint64_t bytesAvailableInBatch = MAX_BYTES_PER_BATCH - totalBytesInBatch; + + batch.vmas[indexInBatch].iov_base = (void*)vmaStart; + + if (vmaSize > bytesAvailableInBatch) { + // VMA would exceed the max available bytes in batch + // clamp with available bytes and finish batch. + vmaSize = bytesAvailableInBatch; + currentOffset_ += bytesAvailableInBatch; + } + + batch.vmas[indexInBatch].iov_len = vmaSize; + totalBytesInBatch += vmaSize; + + ++indexInBatch; + if (totalBytesInBatch >= MAX_BYTES_PER_BATCH) { + // Reached max bytes quota so this marks + // the end of the batch + if (CC_UNLIKELY(vmaSize == (vmas[currentIndex_].end - vmaStart))) { + // we reached max bytes exactly at the end of the vma + // so advance to next one + currentOffset_ = 0; + ++currentIndex_; + } + break; + } + // Fully finished current VMA, move to next one + currentOffset_ = 0; + ++currentIndex_; + } + batch.totalVmas = indexInBatch; + batch.totalBytes = totalBytesInBatch; + if (batch.totalVmas == 0 || batch.totalBytes == 0) { + // This is an empty batch, mark as failed creating. + return false; + } + return true; + } +}; + +// Madvise a set of VMAs given in a batch for a specific process +// The total number of bytes successfully madvised will be set on +// outBytesProcessed. +// Returns 0 on success and standard linux -errno code returned by +// process_madvise on failure +int madviseVmasFromBatch(unique_fd& pidfd, VmaBatch& batch, int madviseType, + uint64_t* outBytesProcessed) { + if (batch.totalVmas == 0 || batch.totalBytes == 0) { + // No VMAs in Batch, skip. + *outBytesProcessed = 0; + return 0; + } + + ATRACE_BEGIN(StringPrintf("Madvise %d: %zu VMAs.", madviseType, batch.totalVmas).c_str()); + int64_t bytesProcessedInSend = + process_madvise(pidfd, batch.vmas, batch.totalVmas, madviseType, 0); + ATRACE_END(); + if (CC_UNLIKELY(bytesProcessedInSend == -1)) { + bytesProcessedInSend = 0; + if (errno != EINVAL) { + // Forward irrecoverable errors and bail out compaction + *outBytesProcessed = 0; + return -errno; + } + } + if (bytesProcessedInSend == 0) { + // When we find a VMA with error, fully consume it as it + // is extremely expensive to iterate on its pages one by one + bytesProcessedInSend = batch.vmas[0].iov_len; + } else if (bytesProcessedInSend < batch.totalBytes) { + // Partially processed the bytes requested + // skip last page which is where it failed. + bytesProcessedInSend += PAGE_SIZE; + } + bytesProcessedInSend = consumeBytes(batch, bytesProcessedInSend); + + *outBytesProcessed = bytesProcessedInSend; + return 0; +} + // Legacy method for compacting processes, any new code should // use compactProcess instead. static inline void compactProcessProcfs(int pid, const std::string& compactionType) { @@ -96,8 +270,6 @@ static inline void compactProcessProcfs(int pid, const std::string& compactionTy // If any VMA fails compaction due to -EINVAL it will be skipped and continue. // However, if it fails for any other reason, it will bail out and forward the error static int64_t compactMemory(const std::vector<Vma>& vmas, int pid, int madviseType) { - static struct iovec vmasToKernel[MAX_VMAS_PER_COMPACTION]; - if (vmas.empty()) { return 0; } @@ -108,13 +280,16 @@ static int64_t compactMemory(const std::vector<Vma>& vmas, int pid, int madviseT return -errno; } - int64_t totalBytesProcessed = 0; + struct iovec destVmas[MAX_VMAS_PER_BATCH]; + + VmaBatch batch; + VmaBatchCreator batcher(&vmas, destVmas); - int64_t vmaOffset = 0; - for (int iVma = 0; iVma < vmas.size();) { - uint64_t bytesSentToCompact = 0; - int iVec = 0; - while (iVec < MAX_VMAS_PER_COMPACTION && iVma < vmas.size()) { + int64_t totalBytesProcessed = 0; + while (batcher.createNextBatch(batch)) { + uint64_t bytesProcessedInSend; + ScopedTrace batchTrace(ATRACE_TAG, "VMA Batch"); + do { if (CC_UNLIKELY(cancelRunningCompaction.load())) { // There could be a significant delay between when a compaction // is requested and when it is handled during this time our @@ -124,50 +299,18 @@ static int64_t compactMemory(const std::vector<Vma>& vmas, int pid, int madviseT StringPrintf("Cancelled compaction for %d", pid).c_str()); return ERROR_COMPACTION_CANCELLED; } - - uint64_t vmaStart = vmas[iVma].start + vmaOffset; - uint64_t vmaSize = vmas[iVma].end - vmaStart; - if (vmaSize == 0) { - goto next_vma; - } - vmasToKernel[iVec].iov_base = (void*)vmaStart; - if (vmaSize > MAX_BYTES_PER_COMPACTION - bytesSentToCompact) { - // Exceeded the max bytes that could be sent, so clamp - // the end to avoid exceeding limit and issue compaction - vmaSize = MAX_BYTES_PER_COMPACTION - bytesSentToCompact; + int error = madviseVmasFromBatch(pidfd, batch, madviseType, &bytesProcessedInSend); + if (error < 0) { + // Returns standard linux errno code + return error; } - - vmasToKernel[iVec].iov_len = vmaSize; - bytesSentToCompact += vmaSize; - ++iVec; - if (bytesSentToCompact >= MAX_BYTES_PER_COMPACTION) { - // Ran out of bytes within iovec, dispatch compaction. - vmaOffset += vmaSize; + if (CC_UNLIKELY(bytesProcessedInSend == 0)) { + // This means there was a problem consuming bytes, + // bail out since no forward progress can be made with this batch break; } - - next_vma: - // Finished current VMA, and have more bytes remaining - vmaOffset = 0; - ++iVma; - } - - ATRACE_BEGIN(StringPrintf("Compact %d VMAs", iVec).c_str()); - auto bytesProcessed = process_madvise(pidfd, vmasToKernel, iVec, madviseType, 0); - ATRACE_END(); - - if (CC_UNLIKELY(bytesProcessed == -1)) { - if (errno == EINVAL) { - // This error is somewhat common due to an unevictable VMA if this is - // the case silently skip the bad VMA and continue compacting the rest. - continue; - } else { - // Forward irrecoverable errors and bail out compaction - return -errno; - } - } - - totalBytesProcessed += bytesProcessed; + totalBytesProcessed += bytesProcessedInSend; + } while (batch.totalBytes > 0 && batch.totalVmas > 0); } return totalBytesProcessed; @@ -203,6 +346,7 @@ static int getAnyPageAdvice(const Vma& vma) { static int64_t compactProcess(int pid, VmaToAdviseFunc vmaToAdviseFunc) { cancelRunningCompaction.store(false); + ATRACE_BEGIN("CollectVmas"); ProcMemInfo meminfo(pid); std::vector<Vma> pageoutVmas, coldVmas; auto vmaCollectorCb = [&coldVmas,&pageoutVmas,&vmaToAdviseFunc](const Vma& vma) { @@ -217,6 +361,7 @@ static int64_t compactProcess(int pid, VmaToAdviseFunc vmaToAdviseFunc) { } }; meminfo.ForEachVmaFromMaps(vmaCollectorCb); + ATRACE_END(); int64_t pageoutBytes = compactMemory(pageoutVmas, pid, MADV_PAGEOUT); if (pageoutBytes < 0) { diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 32adac7f282b..287fb8219650 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -106,7 +106,6 @@ static struct { jmethodID interceptMotionBeforeQueueingNonInteractive; jmethodID interceptKeyBeforeDispatching; jmethodID dispatchUnhandledKey; - jmethodID checkInjectEventsPermission; jmethodID onPointerDisplayIdChanged; jmethodID onPointerDownOutsideFocus; jmethodID getVirtualKeyQuietTimeMillis; @@ -333,7 +332,6 @@ public: bool dispatchUnhandledKey(const sp<IBinder>& token, const KeyEvent* keyEvent, uint32_t policyFlags, KeyEvent* outFallbackKeyEvent) override; void pokeUserActivity(nsecs_t eventTime, int32_t eventType, int32_t displayId) override; - bool checkInjectEventsPermissionNonReentrant(int32_t injectorPid, int32_t injectorUid) override; void onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) override; void setPointerCapture(const PointerCaptureRequest& request) override; void notifyDropWindow(const sp<IBinder>& token, float x, float y) override; @@ -1380,19 +1378,6 @@ void NativeInputManager::pokeUserActivity(nsecs_t eventTime, int32_t eventType, android_server_PowerManagerService_userActivity(eventTime, eventType, displayId); } -bool NativeInputManager::checkInjectEventsPermissionNonReentrant(int32_t injectorPid, - int32_t injectorUid) { - ATRACE_CALL(); - JNIEnv* env = jniEnv(); - jboolean result = - env->CallBooleanMethod(mServiceObj, gServiceClassInfo.checkInjectEventsPermission, - injectorPid, injectorUid); - if (checkAndClearExceptionFromCallback(env, "checkInjectEventsPermission")) { - result = false; - } - return result; -} - void NativeInputManager::onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) { ATRACE_CALL(); JNIEnv* env = jniEnv(); @@ -1709,10 +1694,11 @@ static void nativeSetBlockUntrustedTouchesMode(JNIEnv* env, jobject nativeImplOb } static jint nativeInjectInputEvent(JNIEnv* env, jobject nativeImplObj, jobject inputEventObj, - jint injectorPid, jint injectorUid, jint syncMode, + jboolean injectIntoUid, jint uid, jint syncMode, jint timeoutMillis, jint policyFlags) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + const std::optional<int32_t> targetUid = injectIntoUid ? std::make_optional(uid) : std::nullopt; // static_cast is safe because the value was already checked at the Java layer InputEventInjectionSync mode = static_cast<InputEventInjectionSync>(syncMode); @@ -1725,8 +1711,7 @@ static jint nativeInjectInputEvent(JNIEnv* env, jobject nativeImplObj, jobject i } const InputEventInjectionResult result = - im->getInputManager()->getDispatcher().injectInputEvent(&keyEvent, injectorPid, - injectorUid, mode, + im->getInputManager()->getDispatcher().injectInputEvent(&keyEvent, targetUid, mode, std::chrono::milliseconds( timeoutMillis), uint32_t(policyFlags)); @@ -1739,8 +1724,8 @@ static jint nativeInjectInputEvent(JNIEnv* env, jobject nativeImplObj, jobject i } const InputEventInjectionResult result = - im->getInputManager()->getDispatcher().injectInputEvent(motionEvent, injectorPid, - injectorUid, mode, + im->getInputManager()->getDispatcher().injectInputEvent(motionEvent, targetUid, + mode, std::chrono::milliseconds( timeoutMillis), uint32_t(policyFlags)); @@ -2345,7 +2330,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"setMaximumObscuringOpacityForTouch", "(F)V", (void*)nativeSetMaximumObscuringOpacityForTouch}, {"setBlockUntrustedTouchesMode", "(I)V", (void*)nativeSetBlockUntrustedTouchesMode}, - {"injectInputEvent", "(Landroid/view/InputEvent;IIIII)I", (void*)nativeInjectInputEvent}, + {"injectInputEvent", "(Landroid/view/InputEvent;ZIIII)I", (void*)nativeInjectInputEvent}, {"verifyInputEvent", "(Landroid/view/InputEvent;)Landroid/view/VerifiedInputEvent;", (void*)nativeVerifyInputEvent}, {"toggleCapsLock", "(I)V", (void*)nativeToggleCapsLock}, @@ -2492,9 +2477,6 @@ int register_android_server_InputManager(JNIEnv* env) { "dispatchUnhandledKey", "(Landroid/os/IBinder;Landroid/view/KeyEvent;I)Landroid/view/KeyEvent;"); - GET_METHOD_ID(gServiceClassInfo.checkInjectEventsPermission, clazz, - "checkInjectEventsPermission", "(II)Z"); - GET_METHOD_ID(gServiceClassInfo.onPointerDisplayIdChanged, clazz, "onPointerDisplayIdChanged", "(IFF)V"); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index ceac1023dfb0..870257802608 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1937,6 +1937,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { updatePasswordQualityCacheForUserGroup(userHandle); mPolicyCache.onUserRemoved(userHandle); + if (isManagedProfile(userHandle)) { + clearManagedProfileApnUnchecked(); + } isOrgOwned = mOwners.isProfileOwnerOfOrganizationOwnedDevice(userHandle); mOwners.removeProfileOwner(userHandle); @@ -3116,6 +3119,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { deleteTransferOwnershipBundleLocked(metadata.userId); } updateSystemUpdateFreezePeriodsRecord(/* saveIfChanged */ true); + pushUserControlDisabledPackagesLocked(metadata.userId); } private void maybeLogStart() { @@ -3178,18 +3182,22 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { startOwnerService(userId, "start-user"); } - // TODO(b/218639412): Once PM stores these on a per-user basis, push even empty lists to handle - // DO/PO removal correctly. void pushUserControlDisabledPackagesLocked(int userId) { - if (userId != mOwners.getDeviceOwnerUserId()) { - return; - } - ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked(); - if (deviceOwner == null || deviceOwner.protectedPackages == null) { - return; + final int targetUserId; + final ActiveAdmin owner; + if (getDeviceOwnerUserIdUncheckedLocked() == userId) { + owner = getDeviceOwnerAdminLocked(); + targetUserId = UserHandle.USER_ALL; + } else { + owner = getProfileOwnerAdminLocked(userId); + targetUserId = userId; } - mInjector.getPackageManagerInternal().setDeviceOwnerProtectedPackages( - deviceOwner.info.getPackageName(), deviceOwner.protectedPackages); + + List<String> protectedPackages = (owner == null || owner.protectedPackages == null) + ? Collections.emptyList() : owner.protectedPackages; + mInjector.binderWithCleanCallingIdentity(() -> + mInjector.getPackageManagerInternal().setOwnerProtectedPackages( + targetUserId, protectedPackages)); } @Override @@ -8755,6 +8763,18 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + private void clearManagedProfileApnUnchecked() { + if (!mHasTelephonyFeature) { + return; + } + final List<ApnSetting> apns = getOverrideApnsUnchecked(); + for (ApnSetting apn : apns) { + if (apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) { + removeOverrideApnUnchecked(apn.getId()); + } + } + } + private void clearDeviceOwnerLocked(ActiveAdmin admin, int userId) { mDeviceAdminServiceController.stopServiceForOwner(userId, "clear-device-owner"); @@ -12095,6 +12115,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + private boolean isManagedProfileOwner(CallerIdentity caller) { + return isProfileOwner(caller) && isManagedProfile(caller.getUserId()); + } + private boolean isDefaultSupervisor(CallerIdentity caller) { final String supervisor = mContext.getResources().getString( com.android.internal.R.string.config_defaultSupervisionProfileOwnerComponent); @@ -16295,7 +16319,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final CallerIdentity caller = getCallerIdentity(who); if (apnSetting.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) - || isProfileOwner(caller)); + || isManagedProfileOwner(caller)); } else { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)); } @@ -16323,7 +16347,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { if (apn != null && apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE && apnSetting.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) - || isProfileOwner(caller)); + || isManagedProfileOwner(caller)); } else { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)); } @@ -16351,7 +16375,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { ApnSetting apn = getApnSetting(apnId); if (apn != null && apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) - || isProfileOwner(caller)); + || isManagedProfileOwner(caller)); } else { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)); } @@ -16396,8 +16420,20 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)); - return getOverrideApnsUnchecked(); + Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) + || isManagedProfileOwner(caller)); + List<ApnSetting> apnSettings = getOverrideApnsUnchecked(); + if (isProfileOwner(caller)) { + List<ApnSetting> apnSettingList = new ArrayList<>(); + for (ApnSetting apnSetting : apnSettings) { + if (apnSetting.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) { + apnSettingList.add(apnSetting); + } + } + return apnSettingList; + } else { + return apnSettings; + } } private List<ApnSetting> getOverrideApnsUnchecked() { @@ -16967,23 +17003,20 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); Objects.requireNonNull(packages, "packages is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isFinancedDeviceOwner(caller)); + Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) || isProfileOwner(caller) + || isFinancedDeviceOwner(caller)); checkCanExecuteOrThrowUnsafe( DevicePolicyManager.OPERATION_SET_USER_CONTROL_DISABLED_PACKAGES); synchronized (getLockObject()) { - ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked(); - if (!Objects.equals(deviceOwner.protectedPackages, packages)) { - deviceOwner.protectedPackages = packages.isEmpty() ? null : packages; + ActiveAdmin owner = getDeviceOrProfileOwnerAdminLocked(caller.getUserId()); + if (!Objects.equals(owner.protectedPackages, packages)) { + owner.protectedPackages = packages.isEmpty() ? null : packages; saveSettingsLocked(caller.getUserId()); + pushUserControlDisabledPackagesLocked(caller.getUserId()); } } - mInjector.binderWithCleanCallingIdentity( - () -> mInjector.getPackageManagerInternal().setDeviceOwnerProtectedPackages( - who.getPackageName(), packages)); - DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_USER_CONTROL_DISABLED_PACKAGES) .setAdmin(who) @@ -16996,11 +17029,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isFinancedDeviceOwner(caller)); + Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) || isProfileOwner(caller) + || isFinancedDeviceOwner(caller)); synchronized (getLockObject()) { - ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked(); + ActiveAdmin deviceOwner = getDeviceOrProfileOwnerAdminLocked(caller.getUserId()); return deviceOwner.protectedPackages != null ? deviceOwner.protectedPackages : Collections.emptyList(); } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 80f0186a2528..ef311c249c5f 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1057,6 +1057,7 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartWatchdog"); final Watchdog watchdog = Watchdog.getInstance(); watchdog.start(); + mDumper.addDumpable(watchdog); t.traceEnd(); Slog.i(TAG, "Reading configuration..."); diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index 8461b39f8899..17b42260948d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -3220,34 +3220,19 @@ public class AlarmManagerServiceTest { when(mRoleManager.getRoleHolders(RoleManager.ROLE_SYSTEM_WELLBEING)).thenReturn( Arrays.asList(package4)); - mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, true); - mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = false; - mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] { - package1, - package3, - }); - - // Deny listed packages will be false. - assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package1, uid1)); - assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package2, uid2)); - assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package3, uid3)); - assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4)); - mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, false); - mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true; mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] { package1, package3, }); - // Same as above, deny listed packages will be false. + // Deny listed packages will be false. assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package1, uid1)); assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package2, uid2)); assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package3, uid3)); assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4)); mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, true); - mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true; mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] { package1, package3, diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java index 009dae51e94b..fa8d569d8e3c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java @@ -585,6 +585,7 @@ public final class BackgroundRestrictionTest { DeviceConfigSession<Long> bgCurrentDrainInteractionGracePeriod = null; DeviceConfigSession<Float> bgCurrentDrainRestrictedBucketThreshold = null; DeviceConfigSession<Float> bgCurrentDrainBgRestrictedThreshold = null; + DeviceConfigSession<Boolean> bgCurrentDrainAutoRestrictAbusiveApps = null; DeviceConfigSession<Boolean> bgPromptFgsWithNotiToBgRestricted = null; DeviceConfigSession<Boolean> bgPromptAbusiveAppToBgRestricted = null; DeviceConfigSession<Long> bgNotificationMinInterval = null; @@ -644,6 +645,14 @@ public final class BackgroundRestrictionTest { isLowRamDeviceStatic() ? 1 : 0]); bgCurrentDrainBgRestrictedThreshold.set(bgRestrictedThreshold); + bgCurrentDrainAutoRestrictAbusiveApps = new DeviceConfigSession<>( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED, + DeviceConfig::getBoolean, + mContext.getResources().getBoolean( + R.bool.config_bg_current_drain_auto_restrict_abusive_apps)); + bgCurrentDrainAutoRestrictAbusiveApps.set(true); + bgPromptFgsWithNotiToBgRestricted = new DeviceConfigSession<>( DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, ConstantsObserver.KEY_BG_PROMPT_FGS_WITH_NOTIFICATION_TO_BG_RESTRICTED, @@ -1099,6 +1108,7 @@ public final class BackgroundRestrictionTest { closeIfNotNull(bgCurrentDrainInteractionGracePeriod); closeIfNotNull(bgCurrentDrainRestrictedBucketThreshold); closeIfNotNull(bgCurrentDrainBgRestrictedThreshold); + closeIfNotNull(bgCurrentDrainAutoRestrictAbusiveApps); closeIfNotNull(bgPromptFgsWithNotiToBgRestricted); closeIfNotNull(bgPromptAbusiveAppToBgRestricted); closeIfNotNull(bgNotificationMinInterval); @@ -1651,6 +1661,7 @@ public final class BackgroundRestrictionTest { DeviceConfigSession<Float> bgCurrentDrainBgRestrictedThreshold = null; DeviceConfigSession<Float> bgCurrentDrainRestrictedBucketHighThreshold = null; DeviceConfigSession<Float> bgCurrentDrainBgRestrictedHighThreshold = null; + DeviceConfigSession<Boolean> bgCurrentDrainAutoRestrictAbusiveApps = null; DeviceConfigSession<Long> bgMediaPlaybackMinDurationThreshold = null; DeviceConfigSession<Long> bgLocationMinDurationThreshold = null; DeviceConfigSession<Boolean> bgCurrentDrainEventDurationBasedThresholdEnabled = null; @@ -1736,6 +1747,14 @@ public final class BackgroundRestrictionTest { isLowRamDeviceStatic() ? 1 : 0]); bgCurrentDrainBgRestrictedHighThreshold.set(bgRestrictedHighThreshold); + bgCurrentDrainAutoRestrictAbusiveApps = new DeviceConfigSession<>( + DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, + AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED, + DeviceConfig::getBoolean, + mContext.getResources().getBoolean( + R.bool.config_bg_current_drain_auto_restrict_abusive_apps)); + bgCurrentDrainAutoRestrictAbusiveApps.set(true); + bgMediaPlaybackMinDurationThreshold = new DeviceConfigSession<>( DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_MEDIA_PLAYBACK_MIN_DURATION, @@ -2226,6 +2245,7 @@ public final class BackgroundRestrictionTest { closeIfNotNull(bgCurrentDrainBgRestrictedThreshold); closeIfNotNull(bgCurrentDrainRestrictedBucketHighThreshold); closeIfNotNull(bgCurrentDrainBgRestrictedHighThreshold); + closeIfNotNull(bgCurrentDrainAutoRestrictAbusiveApps); closeIfNotNull(bgMediaPlaybackMinDurationThreshold); closeIfNotNull(bgLocationMinDurationThreshold); closeIfNotNull(bgCurrentDrainEventDurationBasedThresholdEnabled); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java index 444db9128662..da5c8f06bc86 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java @@ -16,6 +16,9 @@ package com.android.server.pm; +import static com.android.server.pm.BackgroundDexOptService.STATUS_DEX_OPT_FAILED; +import static com.android.server.pm.BackgroundDexOptService.STATUS_OK; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -23,12 +26,14 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; +import android.annotation.Nullable; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; @@ -38,10 +43,13 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.HandlerThread; import android.os.PowerManager; +import android.util.Log; +import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalServices; import com.android.server.PinnerService; import com.android.server.pm.dex.DexManager; +import com.android.server.pm.dex.DexoptOptions; import org.junit.After; import org.junit.Before; @@ -52,7 +60,11 @@ import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; @@ -66,8 +78,12 @@ public final class BackgroundDexOptServiceUnitTest { private static final long TEST_WAIT_TIMEOUT_MS = 10_000; - private static final List<String> DEFAULT_PACKAGE_LIST = List.of("aaa", "bbb"); - private static final List<String> EMPTY_PACKAGE_LIST = List.of(); + private static final String PACKAGE_AAA = "aaa"; + private static final List<String> DEFAULT_PACKAGE_LIST = List.of(PACKAGE_AAA, "bbb"); + private int mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; + + // Store expected dexopt sequence for verification. + private ArrayList<DexOptInfo> mDexInfoSequence = new ArrayList<>(); @Mock private Context mContext; @@ -116,14 +132,23 @@ public final class BackgroundDexOptServiceUnitTest { when(mInjector.getDexOptThermalCutoff()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL); when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_NONE); when(mInjector.supportSecondaryDex()).thenReturn(true); - when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST); - when(mDexOptHelper.performDexOptWithStatus(any())).thenReturn( - PackageDexOptimizer.DEX_OPT_PERFORMED); - when(mDexOptHelper.performDexOpt(any())).thenReturn(true); + setupDexOptHelper(); mService = new BackgroundDexOptService(mInjector); } + private void setupDexOptHelper() { + when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST); + when(mDexOptHelper.performDexOptWithStatus(any())).thenAnswer(inv -> { + DexoptOptions opt = inv.getArgument(0); + if (opt.getPackageName().equals(PACKAGE_AAA)) { + return mDexOptResultForPackageAAA; + } + return PackageDexOptimizer.DEX_OPT_PERFORMED; + }); + when(mDexOptHelper.performDexOpt(any())).thenReturn(true); + } + @After public void tearDown() throws Exception { LocalServices.removeServiceForTest(BackgroundDexOptService.class); @@ -159,7 +184,7 @@ public final class BackgroundDexOptServiceUnitTest { @Test public void testNoExecutionForNoOptimizablePackages() { initUntilBootCompleted(); - when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(EMPTY_PACKAGE_LIST); + when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(Collections.emptyList()); assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isFalse(); @@ -170,15 +195,70 @@ public final class BackgroundDexOptServiceUnitTest { public void testPostBootUpdateFullRun() { initUntilBootCompleted(); - runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1); + runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, + /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); + } + + @Test + public void testPostBootUpdateFullRunWithPackageFailure() { + mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED; + + initUntilBootCompleted(); + + runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED, + /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); + + assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); + assertThat(getFailedPackageNamesSecondary()).isEmpty(); } @Test public void testIdleJobFullRun() { initUntilBootCompleted(); + runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, + /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); + runFullJob(mJobServiceForIdle, mJobParametersForIdle, + /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); + } + + @Test + public void testIdleJobFullRunWithFailureOnceAndSuccessAfterUpdate() { + mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED; + + initUntilBootCompleted(); + + runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, + /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED, + /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); + + assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); + assertThat(getFailedPackageNamesSecondary()).isEmpty(); + + runFullJob(mJobServiceForIdle, mJobParametersForIdle, + /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); + + assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); + assertThat(getFailedPackageNamesSecondary()).isEmpty(); + + mService.notifyPackageChanged(PACKAGE_AAA); - runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1); - runFullJob(mJobServiceForIdle, mJobParametersForIdle, true, 2); + assertThat(getFailedPackageNamesPrimary()).isEmpty(); + assertThat(getFailedPackageNamesSecondary()).isEmpty(); + + // Succeed this time. + mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; + + runFullJob(mJobServiceForIdle, mJobParametersForIdle, + /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, + /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null); + + assertThat(getFailedPackageNamesPrimary()).isEmpty(); + assertThat(getFailedPackageNamesSecondary()).isEmpty(); } @Test @@ -404,8 +484,10 @@ public final class BackgroundDexOptServiceUnitTest { } private void runFullJob(BackgroundDexOptJobService jobService, JobParameters params, - boolean expectedReschedule, int totalJobRuns) { + boolean expectedReschedule, int expectedStatus, int totalJobFinishedWithParams, + @Nullable String expectedSkippedPackage) { when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); + addFullRunSequence(expectedSkippedPackage); assertThat(mService.onStartJob(jobService, params)).isTrue(); ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class); @@ -413,20 +495,99 @@ public final class BackgroundDexOptServiceUnitTest { argThreadRunnable.getValue().run(); - verify(jobService).jobFinished(params, expectedReschedule); + verify(jobService, times(totalJobFinishedWithParams)).jobFinished(params, + expectedReschedule); // Never block verify(mDexOptHelper, never()).controlDexOptBlocking(true); - verifyPerformDexOpt(DEFAULT_PACKAGE_LIST, totalJobRuns); + verifyPerformDexOpt(); + assertThat(getLastExecutionStatus()).isEqualTo(expectedStatus); } - private void verifyPerformDexOpt(List<String> pkgs, int expectedRuns) { + private void verifyPerformDexOpt() { InOrder inOrder = inOrder(mDexOptHelper); - for (int i = 0; i < expectedRuns; i++) { - for (String pkg : pkgs) { - inOrder.verify(mDexOptHelper, times(1)).performDexOptWithStatus(argThat((option) -> - option.getPackageName().equals(pkg) && !option.isDexoptOnlySecondaryDex())); - inOrder.verify(mDexOptHelper, times(1)).performDexOpt(argThat((option) -> - option.getPackageName().equals(pkg) && option.isDexoptOnlySecondaryDex())); + inOrder.verify(mDexOptHelper).getOptimizablePackages(any()); + for (DexOptInfo info : mDexInfoSequence) { + if (info.isPrimary) { + verify(mDexOptHelper).performDexOptWithStatus( + argThat((option) -> option.getPackageName().equals(info.packageName) + && !option.isDexoptOnlySecondaryDex())); + } else { + inOrder.verify(mDexOptHelper).performDexOpt( + argThat((option) -> option.getPackageName().equals(info.packageName) + && option.isDexoptOnlySecondaryDex())); + } + } + + // Even InOrder cannot check the order if the same call is made multiple times. + // To check the order across multiple runs, we reset the mock so that order can be checked + // in each call. + mDexInfoSequence.clear(); + reset(mDexOptHelper); + setupDexOptHelper(); + } + + private String findDumpValueForKey(String key) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(out, true); + IndentingPrintWriter writer = new IndentingPrintWriter(pw, ""); + try { + mService.dump(writer); + writer.flush(); + Log.i(TAG, "dump output:" + out.toString()); + for (String line : out.toString().split(System.lineSeparator())) { + String[] vals = line.split(":"); + if (vals[0].equals(key)) { + if (vals.length == 2) { + return vals[1].strip(); + } else { + break; + } + } + } + return ""; + } finally { + writer.close(); + } + } + + List<String> findStringListFromDump(String key) { + String values = findDumpValueForKey(key); + if (values.isEmpty()) { + return Collections.emptyList(); + } + return Arrays.asList(values.split(",")); + } + + private List<String> getFailedPackageNamesPrimary() { + return findStringListFromDump("mFailedPackageNamesPrimary"); + } + + private List<String> getFailedPackageNamesSecondary() { + return findStringListFromDump("mFailedPackageNamesSecondary"); + } + + private int getLastExecutionStatus() { + return Integer.parseInt(findDumpValueForKey("mLastExecutionStatus")); + } + + private static class DexOptInfo { + public final String packageName; + public final boolean isPrimary; + + private DexOptInfo(String packageName, boolean isPrimary) { + this.packageName = packageName; + this.isPrimary = isPrimary; + } + } + + private void addFullRunSequence(@Nullable String expectedSkippedPackage) { + for (String packageName : DEFAULT_PACKAGE_LIST) { + if (packageName.equals(expectedSkippedPackage)) { + // only fails primary dexopt in mocking but add secodary + mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false)); + } else { + mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ true)); + mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false)); } } } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt index c9598bdc8823..e30f3d26119f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt @@ -69,13 +69,34 @@ class DeletePackageHelperTest { } @Test - fun deleteSystemPackageFailsIfNotAdmin() { + fun deleteSystemPackageFailsIfNotAdminAndNotProfile() { val ps = mPms.mSettings.getPackageLPr("a.data.package") whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true) whenever(mUserManagerInternal.getUserInfo(1)).thenReturn(UserInfo(1, "test", 0)) + whenever(mUserManagerInternal.getProfileParentId(1)).thenReturn(1) val dph = DeletePackageHelper(mPms) - val result = dph.deletePackageX("a.data.package", 1L, 1, 0, false) + val result = dph.deletePackageX("a.data.package", 1L, 1, + PackageManager.DELETE_SYSTEM_APP, false) + + assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED) + } + + @Test + fun deleteSystemPackageFailsIfProfileOfNonAdmin() { + val userId = 1 + val parentId = 5 + val ps = mPms.mSettings.getPackageLPr("a.data.package") + whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true) + whenever(mUserManagerInternal.getUserInfo(userId)).thenReturn( + UserInfo(userId, "test", UserInfo.FLAG_PROFILE)) + whenever(mUserManagerInternal.getProfileParentId(userId)).thenReturn(parentId) + whenever(mUserManagerInternal.getUserInfo(parentId)).thenReturn( + UserInfo(userId, "testparent", 0)) + + val dph = DeletePackageHelper(mPms) + val result = dph.deletePackageX("a.data.package", 1L, userId, + PackageManager.DELETE_SYSTEM_APP, false) assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED) } @@ -93,4 +114,23 @@ class DeletePackageHelperTest { assertThat(result).isEqualTo(PackageManager.DELETE_SUCCEEDED) } + + @Test + fun deleteSystemPackageSucceedsIfProfileOfAdmin() { + val userId = 1 + val parentId = 5 + val ps = mPms.mSettings.getPackageLPr("a.data.package") + whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true) + whenever(mUserManagerInternal.getUserInfo(userId)).thenReturn( + UserInfo(userId, "test", UserInfo.FLAG_PROFILE)) + whenever(mUserManagerInternal.getProfileParentId(userId)).thenReturn(parentId) + whenever(mUserManagerInternal.getUserInfo(parentId)).thenReturn( + UserInfo(userId, "testparent", UserInfo.FLAG_ADMIN)) + + val dph = DeletePackageHelper(mPms) + val result = dph.deletePackageX("a.data.package", 1L, userId, + PackageManager.DELETE_SYSTEM_APP, false) + + assertThat(result).isEqualTo(PackageManager.DELETE_SUCCEEDED) + } }
\ No newline at end of file diff --git a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java index fa3e05a6b001..20cfd59973c3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java @@ -24,20 +24,33 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import android.app.AppOpsManager; import android.content.Context; +import android.content.res.Resources; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; import android.hardware.lights.Light; +import android.hardware.lights.LightState; import android.hardware.lights.LightsManager; import android.hardware.lights.LightsRequest; +import android.os.Handler; +import android.os.Looper; import android.permission.PermissionManager; import android.util.ArraySet; import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.R; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -53,26 +66,43 @@ import java.util.stream.Collectors; public class CameraPrivacyLightControllerTest { + private int mDayColor = 1; + private int mNightColor = 0; + private int mCameraPrivacyLightAlsAveragingIntervalMillis = 5000; + private int mCameraPrivacyLightAlsNightThreshold = (int) getLightSensorValue(15); + private MockitoSession mMockitoSession; @Mock private Context mContext; @Mock + private Resources mResources; + + @Mock private LightsManager mLightsManager; @Mock private AppOpsManager mAppOpsManager; @Mock + private SensorManager mSensorManager; + + @Mock private LightsManager.LightsSession mLightsSession; + @Mock + private Sensor mLightSensor; + private ArgumentCaptor<AppOpsManager.OnOpActiveChangedListener> mAppOpsListenerCaptor = ArgumentCaptor.forClass(AppOpsManager.OnOpActiveChangedListener.class); private ArgumentCaptor<LightsRequest> mLightsRequestCaptor = ArgumentCaptor.forClass(LightsRequest.class); + private ArgumentCaptor<SensorEventListener> mLightSensorListenerCaptor = + ArgumentCaptor.forClass(SensorEventListener.class); + private Set<String> mExemptedPackages = new ArraySet<>(); private List<Light> mLights = new ArrayList<>(); @@ -86,11 +116,22 @@ public class CameraPrivacyLightControllerTest { .spyStatic(PermissionManager.class) .startMocking(); + doReturn(mDayColor).when(mContext).getColor(R.color.camera_privacy_light_day); + doReturn(mNightColor).when(mContext).getColor(R.color.camera_privacy_light_night); + + doReturn(mResources).when(mContext).getResources(); + doReturn(mCameraPrivacyLightAlsAveragingIntervalMillis).when(mResources) + .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis); + doReturn(mCameraPrivacyLightAlsNightThreshold).when(mResources) + .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold); + doReturn(mLightsManager).when(mContext).getSystemService(LightsManager.class); doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class); + doReturn(mSensorManager).when(mContext).getSystemService(SensorManager.class); doReturn(mLights).when(mLightsManager).getLights(); doReturn(mLightsSession).when(mLightsManager).openSession(anyInt()); + doReturn(mLightSensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); doReturn(mExemptedPackages) .when(() -> PermissionManager.getIndicatorExemptedPackages(any())); @@ -107,7 +148,7 @@ public class CameraPrivacyLightControllerTest { @Test public void testAppsOpsListenerNotRegisteredWithoutCameraLights() { mLights.add(getNextLight(false)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager, times(0)).startWatchingActive(any(), any(), any()); } @@ -116,7 +157,7 @@ public class CameraPrivacyLightControllerTest { public void testAppsOpsListenerRegisteredWithCameraLight() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager, times(1)).startWatchingActive(any(), any(), any()); } @@ -128,14 +169,13 @@ public class CameraPrivacyLightControllerTest { mLights.add(getNextLight(r.nextBoolean())); } - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); // Verify no session has been opened at this point. verify(mLightsManager, times(0)).openSession(anyInt()); // Set camera op as active. - verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); - mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10101, "pkg1", true); + openCamera(); // Verify session has been opened exactly once verify(mLightsManager, times(1)).openSession(anyInt()); @@ -161,7 +201,7 @@ public class CameraPrivacyLightControllerTest { public void testWillOnlyOpenOnceWhenTwoPackagesStartOp() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); @@ -176,7 +216,7 @@ public class CameraPrivacyLightControllerTest { public void testWillCloseOnFinishOp() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); @@ -192,7 +232,7 @@ public class CameraPrivacyLightControllerTest { public void testWillCloseOnFinishOpForAllPackages() { mLights.add(getNextLight(true)); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); int numUids = 100; List<Integer> uids = new ArrayList<>(numUids); @@ -226,7 +266,7 @@ public class CameraPrivacyLightControllerTest { mLights.add(getNextLight(true)); mExemptedPackages.add("pkg1"); - new CameraPrivacyLightController(mContext); + createCameraPrivacyLightController(); verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); @@ -235,6 +275,147 @@ public class CameraPrivacyLightControllerTest { verify(mLightsManager, times(0)).openSession(anyInt()); } + @Test + public void testNoLightSensor() { + mLights.add(getNextLight(true)); + doReturn(null).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + createCameraPrivacyLightController(); + + openCamera(); + + verify(mLightsSession).requestLights(mLightsRequestCaptor.capture()); + LightsRequest lightsRequest = mLightsRequestCaptor.getValue(); + for (LightState lightState : lightsRequest.getLightStates()) { + assertEquals(mDayColor, lightState.getColor()); + } + } + + @Test + public void testALSListenerNotRegisteredUntilCameraIsOpened() { + mLights.add(getNextLight(true)); + Sensor sensor = mock(Sensor.class); + doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + CameraPrivacyLightController cplc = createCameraPrivacyLightController(); + + verify(mSensorManager, never()).registerListener(any(SensorEventListener.class), + any(Sensor.class), anyInt(), any(Handler.class)); + + openCamera(); + + verify(mSensorManager, times(1)).registerListener(mLightSensorListenerCaptor.capture(), + any(Sensor.class), anyInt(), any(Handler.class)); + + mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", false); + verify(mSensorManager, times(1)).unregisterListener(mLightSensorListenerCaptor.getValue()); + } + + @Ignore + @Test + public void testDayColor() { + testBrightnessToColor(20, mDayColor); + } + + @Ignore + @Test + public void testNightColor() { + testBrightnessToColor(10, mNightColor); + } + + private void testBrightnessToColor(int brightnessValue, int color) { + mLights.add(getNextLight(true)); + Sensor sensor = mock(Sensor.class); + doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + CameraPrivacyLightController cplc = createCameraPrivacyLightController(); + cplc.setElapsedRealTime(0); + + openCamera(); + + verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(), + any(Sensor.class), anyInt(), any(Handler.class)); + SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue(); + float[] sensorEventValues = new float[1]; + SensorEvent sensorEvent = new SensorEvent(sensor, 0, 0, sensorEventValues); + + sensorEventValues[0] = getLightSensorValue(brightnessValue); + sensorListener.onSensorChanged(sensorEvent); + + verify(mLightsSession, atLeastOnce()).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(color, lightState.getColor()); + } + } + + @Ignore + @Test + public void testDayToNightTransistion() { + mLights.add(getNextLight(true)); + Sensor sensor = mock(Sensor.class); + doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT); + + CameraPrivacyLightController cplc = createCameraPrivacyLightController(); + cplc.setElapsedRealTime(0); + + openCamera(); + // There will be an initial call at brightness 0 + verify(mLightsSession, times(1)).requestLights(any(LightsRequest.class)); + + verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(), + any(Sensor.class), anyInt(), any(Handler.class)); + SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue(); + + onSensorEvent(cplc, sensorListener, sensor, 0, 20); + + // 5 sec avg = 20 + onSensorEvent(cplc, sensorListener, sensor, 5000, 30); + + verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(mDayColor, lightState.getColor()); + } + + // 5 sec avg = 22 + + onSensorEvent(cplc, sensorListener, sensor, 6000, 10); + + // 5 sec avg = 18 + + onSensorEvent(cplc, sensorListener, sensor, 8000, 5); + + // Should have always been day + verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(mDayColor, lightState.getColor()); + } + + // 5 sec avg = 12 + + onSensorEvent(cplc, sensorListener, sensor, 10000, 5); + + // Should now be night + verify(mLightsSession, times(3)).requestLights(mLightsRequestCaptor.capture()); + for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) { + assertEquals(mNightColor, lightState.getColor()); + } + } + + private void onSensorEvent(CameraPrivacyLightController cplc, + SensorEventListener sensorListener, Sensor sensor, long timestamp, int value) { + cplc.setElapsedRealTime(timestamp); + sensorListener.onSensorChanged(new SensorEvent(sensor, 0, timestamp, + new float[] {getLightSensorValue(value)})); + } + + // Use the test thread so that the test is deterministic + private CameraPrivacyLightController createCameraPrivacyLightController() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + return new CameraPrivacyLightController(mContext, Looper.myLooper()); + } + private Light getNextLight(boolean cameraType) { Light light = ExtendedMockito.mock(Light.class); if (cameraType) { @@ -245,4 +426,13 @@ public class CameraPrivacyLightControllerTest { doReturn(mNextLightId++).when(light).getId(); return light; } + + private float getLightSensorValue(int i) { + return (float) Math.exp(i / CameraPrivacyLightController.LIGHT_VALUE_MULTIPLIER); + } + + private void openCamera() { + verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture()); + mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", true); + } } diff --git a/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java index 18e0f29d4166..bce99a09c6d2 100644 --- a/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java +++ b/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java @@ -416,6 +416,7 @@ public class BroadcastRecordTest { null /* resolvedType */, null /* requiredPermissions */, null /* excludedPermissions */, + null /* excludedPackages */, 0 /* appOp */, null /* options */, new ArrayList<>(receivers), // Make a copy to not affect the original list. diff --git a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java index 5b3a1284069e..98f0603ca633 100644 --- a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java @@ -339,7 +339,7 @@ public final class AppHibernationServiceTest { ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); verify(mIActivityManager, times(2)).broadcastIntentWithFeature(any(), any(), intentArgumentCaptor.capture(), any(), any(), anyInt(), any(), any(), any(), any(), - anyInt(), any(), anyBoolean(), anyBoolean(), eq(USER_ID_1)); + any(), anyInt(), any(), anyBoolean(), anyBoolean(), eq(USER_ID_1)); List<Intent> capturedIntents = intentArgumentCaptor.getAllValues(); assertEquals(capturedIntents.get(0).getAction(), Intent.ACTION_LOCKED_BOOT_COMPLETED); assertEquals(capturedIntents.get(1).getAction(), Intent.ACTION_BOOT_COMPLETED); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index 77cbb3a6398c..5d9d7656aa5b 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -34,6 +34,9 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.Display; import android.view.DisplayInfo; +import android.view.WindowManager; + +import androidx.test.InstrumentationRegistry; import com.android.server.LocalServices; @@ -79,7 +82,9 @@ public class InputControllerTest { // Allow virtual devices to be created on the looper thread for testing. final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true; mInputController = new InputController(new Object(), mNativeWrapperMock, - new Handler(TestableLooper.get(this).getLooper()), threadVerifier); + new Handler(TestableLooper.get(this).getLooper()), + InstrumentationRegistry.getTargetContext().getSystemService(WindowManager.class), + threadVerifier); } @Test diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index cbb9fd7c30dd..f9671e56fe12 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -77,6 +77,7 @@ import android.testing.TestableLooper; import android.util.ArraySet; import android.view.DisplayInfo; import android.view.KeyEvent; +import android.view.WindowManager; import androidx.test.InstrumentationRegistry; @@ -208,7 +209,8 @@ public class VirtualDeviceManagerServiceTest { // Allow virtual devices to be created on the looper thread for testing. final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true; mInputController = new InputController(new Object(), mNativeWrapperMock, - new Handler(TestableLooper.get(this).getLooper()), threadVerifier); + new Handler(TestableLooper.get(this).getLooper()), + mContext.getSystemService(WindowManager.class), threadVerifier); mAssociationInfo = new AssociationInfo(1, 0, null, MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 0fc201eae3d4..ec6b67450a1a 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -6996,22 +6996,23 @@ public class DevicePolicyManagerTest extends DpmTestBase { dpm.setUserControlDisabledPackages(admin1, testPackages); verify(getServices().packageManagerInternal) - .setDeviceOwnerProtectedPackages(admin1.getPackageName(), testPackages); + .setOwnerProtectedPackages(UserHandle.USER_ALL, testPackages); assertThat(dpm.getUserControlDisabledPackages(admin1)).isEqualTo(testPackages); } @Test - public void testSetUserControlDisabledPackages_failingAsPO() { + public void testSetUserControlDisabledPackages_asPO() { final List<String> testPackages = new ArrayList<>(); testPackages.add("package_1"); testPackages.add("package_2"); mServiceContext.permissions.add(permission.MANAGE_DEVICE_ADMINS); setAsProfileOwner(admin1); - assertExpectException(SecurityException.class, /* messageRegex= */ null, - () -> dpm.setUserControlDisabledPackages(admin1, testPackages)); - assertExpectException(SecurityException.class, /* messageRegex= */ null, - () -> dpm.getUserControlDisabledPackages(admin1)); + dpm.setUserControlDisabledPackages(admin1, testPackages); + + verify(getServices().packageManagerInternal) + .setOwnerProtectedPackages(CALLER_USER_HANDLE, testPackages); + assertThat(dpm.getUserControlDisabledPackages(admin1)).isEqualTo(testPackages); } private void configureProfileOwnerOfOrgOwnedDevice(ComponentName who, int userId) { @@ -7845,7 +7846,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { dpm.setUserControlDisabledPackages(admin1, packages); verify(getServices().packageManagerInternal) - .setDeviceOwnerProtectedPackages(eq(admin1.getPackageName()), eq(packages)); + .setOwnerProtectedPackages(eq(UserHandle.USER_ALL), eq(packages)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index f27b8c2f4b3a..8112ca8fbb14 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -560,8 +560,19 @@ public class HdmiCecLocalDeviceTvTest { HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated( ADDR_TV, ADDR_AUDIO_SYSTEM); - assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); + // <Report ARC Initiated> should only be sent after SAD querying is done + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated); + + // Finish querying SADs assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java index f7983ca21816..3228e82b566b 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java @@ -139,11 +139,12 @@ public class RequestSadActionTest { } @Test - public void noResponse_queryAgain_emptyResult() { + public void noResponse_queryAgainOnce_emptyResult() { RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM, mCallback); action.start(); mTestLooper.dispatchAll(); + assertThat(mSupportedSads).isNull(); HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, @@ -153,45 +154,90 @@ public class RequestSadActionTest { mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); assertThat(mNativeWrapper.getResultMessages()).contains(expected1); - mNativeWrapper.clearResultMessages(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); + assertThat(mSupportedSads).isNotNull(); + assertThat(mSupportedSads.size()).isEqualTo(0); + HdmiCecMessage expected2 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, CODECS_TO_QUERY_2.stream().mapToInt(i -> i).toArray()); - assertThat(mNativeWrapper.getResultMessages()).contains(expected2); - mNativeWrapper.clearResultMessages(); + HdmiCecMessage expected3 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( + mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, + CODECS_TO_QUERY_3.stream().mapToInt(i -> i).toArray()); + HdmiCecMessage expected4 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( + mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, + CODECS_TO_QUERY_4.stream().mapToInt(i -> i).toArray()); + + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected2); - mNativeWrapper.clearResultMessages(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); - - HdmiCecMessage expected3 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( - mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, - CODECS_TO_QUERY_3.stream().mapToInt(i -> i).toArray()); - assertThat(mNativeWrapper.getResultMessages()).contains(expected3); - mNativeWrapper.clearResultMessages(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected3); - mNativeWrapper.clearResultMessages(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected2); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected3); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected4); + assertThat(mSupportedSads.size()).isEqualTo(0); + } + + @Test + public void unrecognizedOpcode_dontQueryAgain_emptyResult() { + RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM, + mCallback); + action.start(); + mTestLooper.dispatchAll(); + assertThat(mSupportedSads).isNull(); + + HdmiCecMessage unrecognizedOpcode = HdmiCecMessageBuilder.buildFeatureAbortCommand( + Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, + Constants.MESSAGE_REQUEST_SHORT_AUDIO_DESCRIPTOR, + Constants.ABORT_UNRECOGNIZED_OPCODE); + + HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( + mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, + CODECS_TO_QUERY_1.stream().mapToInt(i -> i).toArray()); + assertThat(mNativeWrapper.getResultMessages()).contains(expected1); + action.processCommand(unrecognizedOpcode); + mTestLooper.dispatchAll(); + + assertThat(mSupportedSads).isNotNull(); + assertThat(mSupportedSads.size()).isEqualTo(0); + + HdmiCecMessage expected2 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( + mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, + CODECS_TO_QUERY_2.stream().mapToInt(i -> i).toArray()); + HdmiCecMessage expected3 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( + mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, + CODECS_TO_QUERY_3.stream().mapToInt(i -> i).toArray()); HdmiCecMessage expected4 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, CODECS_TO_QUERY_4.stream().mapToInt(i -> i).toArray()); - assertThat(mNativeWrapper.getResultMessages()).contains(expected4); - mNativeWrapper.clearResultMessages(); + + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected4); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected2); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected3); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected4); assertThat(mSupportedSads.size()).isEqualTo(0); } @@ -455,11 +501,12 @@ public class RequestSadActionTest { } @Test - public void invalidMessageLength_queryAgain() { + public void invalidMessageLength_queryAgainOnce() { RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM, mCallback); action.start(); mTestLooper.dispatchAll(); + assertThat(mSupportedSads).isNull(); HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, @@ -482,63 +529,35 @@ public class RequestSadActionTest { mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); + assertThat(mSupportedSads).isNotNull(); + assertThat(mSupportedSads.size()).isEqualTo(0); + HdmiCecMessage expected2 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, CODECS_TO_QUERY_2.stream().mapToInt(i -> i).toArray()); - byte[] sadsToRespond_2 = new byte[]{ - 0x05, 0x18, 0x4A, - 0x06, 0x64, 0x5A, - 0x07, - 0x08, 0x20, 0x0A}; - HdmiCecMessage response2 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor( - Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_2); - assertThat(mNativeWrapper.getResultMessages()).contains(expected2); - mNativeWrapper.clearResultMessages(); - action.processCommand(response2); - mTestLooper.dispatchAll(); - mTestLooper.moveTimeForward(TIMEOUT_MS); - mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected2); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(TIMEOUT_MS); - mTestLooper.dispatchAll(); - HdmiCecMessage expected3 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, CODECS_TO_QUERY_3.stream().mapToInt(i -> i).toArray()); - byte[] sadsToRespond_3 = new byte[0]; - HdmiCecMessage response3 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor( - Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_3); - assertThat(mNativeWrapper.getResultMessages()).contains(expected3); - mNativeWrapper.clearResultMessages(); - action.processCommand(response3); + HdmiCecMessage expected4 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( + mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, + CODECS_TO_QUERY_4.stream().mapToInt(i -> i).toArray()); + + mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected3); - mNativeWrapper.clearResultMessages(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); - - HdmiCecMessage expected4 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( - mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, - CODECS_TO_QUERY_4.stream().mapToInt(i -> i).toArray()); - byte[] sadsToRespond_4 = new byte[]{ - 0x0D, 0x18, 0x4A, - 0x0E, 0x64, 0x5A, - 0x0F, 0x4B}; - HdmiCecMessage response4 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor( - Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_4); - assertThat(mNativeWrapper.getResultMessages()).contains(expected4); - mNativeWrapper.clearResultMessages(); - action.processCommand(response4); + mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected4); mTestLooper.moveTimeForward(TIMEOUT_MS); mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected2); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected3); + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected4); assertThat(mSupportedSads.size()).isEqualTo(0); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 80dee10b3b7f..a7dc8518daea 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -9573,6 +9573,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMaybeShowReviewPermissionsNotification_unknown() { + reset(mMockNm); + // Set up various possible states of the settings int and confirm whether or not the // notification is shown as expected @@ -9586,6 +9588,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMaybeShowReviewPermissionsNotification_shouldShow() { + reset(mMockNm); + // If state is SHOULD_SHOW, it ... should show Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, @@ -9598,6 +9602,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMaybeShowReviewPermissionsNotification_alreadyShown() { + reset(mMockNm); + // If state is either USER_INTERACTED or DISMISSED, we should not show this on boot Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE, @@ -9614,6 +9620,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testMaybeShowReviewPermissionsNotification_reshown() { + reset(mMockNm); + // If we have re-shown the notification and the user did not subsequently interacted with // it, then make sure we show when trying on boot Settings.Global.putInt(mContext.getContentResolver(), @@ -9627,6 +9635,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testRescheduledReviewPermissionsNotification() { + reset(mMockNm); + // when rescheduled, the notification goes through the NotificationManagerInternal service // this call doesn't need to know anything about previously scheduled state -- if called, // it should send the notification & write the appropriate int to Settings diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java index 3a352cbe1900..4c7e8433b15b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java @@ -17,7 +17,6 @@ package com.android.server.notification; import static android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT; import static android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED; -import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET; import static android.content.pm.PackageManager.GET_PERMISSIONS; @@ -26,8 +25,6 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.google.common.truth.Truth.assertThat; -import static junit.framework.Assert.fail; - import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -54,7 +51,6 @@ import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; @@ -62,14 +58,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.lang.reflect.Type; -import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; @SmallTest @@ -88,7 +77,7 @@ public class PermissionHelperTest extends UiServiceTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager, false); + mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager); PackageInfo testPkgInfo = new PackageInfo(); testPkgInfo.requestedPermissions = new String[]{ Manifest.permission.POST_NOTIFICATIONS }; when(mPackageManager.getPackageInfo(anyString(), anyLong(), anyInt())) @@ -97,12 +86,12 @@ public class PermissionHelperTest extends UiServiceTestCase { @Test public void testHasPermission() throws Exception { - when(mPmi.checkPostNotificationsPermissionGrantedOrLegacyAccess(anyInt())) + when(mPmi.checkUidPermission(anyInt(), anyString())) .thenReturn(PERMISSION_GRANTED); assertThat(mPermissionHelper.hasPermission(1)).isTrue(); - when(mPmi.checkPostNotificationsPermissionGrantedOrLegacyAccess(anyInt())) + when(mPmi.checkUidPermission(anyInt(), anyString())) .thenReturn(PERMISSION_DENIED); assertThat(mPermissionHelper.hasPermission(1)).isFalse(); @@ -194,79 +183,12 @@ public class PermissionHelperTest extends UiServiceTestCase { verify(mPermManager).grantRuntimePermission( "pkg", Manifest.permission.POST_NOTIFICATIONS, 10); verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, - FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED, - FLAG_PERMISSION_USER_SET, true, 10); - } - - @Test - public void testSetNotificationPermission_grantReviewRequired() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) - .thenReturn(PERMISSION_DENIED); - - mPermissionHelper.setNotificationPermission("pkg", 10, true, false, true); - - verify(mPermManager, never()).revokeRuntimePermission( - "pkg", Manifest.permission.POST_NOTIFICATIONS, 10, "PermissionHelper"); - verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, - FLAG_PERMISSION_REVIEW_REQUIRED, FLAG_PERMISSION_REVIEW_REQUIRED, true, 10); - } - - @Test - public void testSetNotificationPermission_pkgPerm_grantReviewRequired() throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) - .thenReturn(PERMISSION_DENIED); - - PermissionHelper.PackagePermission pkgPerm = new PermissionHelper.PackagePermission( - "pkg", 10, true, false); - mPermissionHelper.setNotificationPermission(pkgPerm); - - verify(mPermManager, never()).revokeRuntimePermission( - "pkg", Manifest.permission.POST_NOTIFICATIONS, 10, "PermissionHelper"); - verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, - FLAG_PERMISSION_REVIEW_REQUIRED, FLAG_PERMISSION_REVIEW_REQUIRED, true, 10); - } - - @Test - public void testSetNotificationPermission_pkgPerm_notUserSet_grantedByDefaultPermNotSet() - throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) - .thenReturn(PERMISSION_DENIED); - when(mPermManager.getPermissionFlags(anyString(), - eq(Manifest.permission.POST_NOTIFICATIONS), - anyInt())).thenReturn(FLAG_PERMISSION_GRANTED_BY_DEFAULT); - PermissionHelper.PackagePermission pkgPerm = new PermissionHelper.PackagePermission( - "pkg", 10, true, false); - - mPermissionHelper.setNotificationPermission(pkgPerm); - verify(mPermManager, never()).revokeRuntimePermission( - anyString(), anyString(), anyInt(), anyString()); - verify(mPermManager, never()).updatePermissionFlags( - anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt()); - } - - @Test - public void testSetNotificationPermission_pkgPerm_userSet_grantedByDefaultPermSet() - throws Exception { - when(mPmi.checkPermission(anyString(), anyString(), anyInt())) - .thenReturn(PERMISSION_DENIED); - when(mPermManager.getPermissionFlags(anyString(), - eq(Manifest.permission.POST_NOTIFICATIONS), - anyInt())).thenReturn(FLAG_PERMISSION_GRANTED_BY_DEFAULT); - PermissionHelper.PackagePermission pkgPerm = new PermissionHelper.PackagePermission( - "pkg", 10, true, true); - - mPermissionHelper.setNotificationPermission(pkgPerm); - verify(mPermManager).grantRuntimePermission( - "pkg", Manifest.permission.POST_NOTIFICATIONS, 10); - verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, - FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED, - FLAG_PERMISSION_USER_SET, true, 10); + FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10); } @Test public void testSetNotificationPermission_pkgPerm_grantedByDefaultPermSet_allUserSet() throws Exception { - mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager, true); when(mPmi.checkPermission(anyString(), anyString(), anyInt())) .thenReturn(PERMISSION_DENIED); when(mPermManager.getPermissionFlags(anyString(), @@ -279,8 +201,7 @@ public class PermissionHelperTest extends UiServiceTestCase { verify(mPermManager).grantRuntimePermission( "pkg", Manifest.permission.POST_NOTIFICATIONS, 10); verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, - FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED, - FLAG_PERMISSION_USER_SET, true, 10); + FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10); } @Test @@ -293,8 +214,7 @@ public class PermissionHelperTest extends UiServiceTestCase { verify(mPermManager).revokeRuntimePermission( eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString()); verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, - FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED, - FLAG_PERMISSION_USER_SET, true, 10); + FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10); } @Test @@ -306,8 +226,8 @@ public class PermissionHelperTest extends UiServiceTestCase { verify(mPermManager).grantRuntimePermission( "pkg", Manifest.permission.POST_NOTIFICATIONS, 10); - verify(mPermManager, never()).updatePermissionFlags( - anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt()); + verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, + 0, FLAG_PERMISSION_USER_SET, true, 10); } @Test @@ -319,8 +239,8 @@ public class PermissionHelperTest extends UiServiceTestCase { verify(mPermManager).revokeRuntimePermission( eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString()); - verify(mPermManager, never()).updatePermissionFlags( - anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt()); + verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS, + 0, FLAG_PERMISSION_USER_SET, true, 10); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index a5cec7e01e9a..8d50ceaf74e9 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -85,6 +85,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Color; import android.media.AudioAttributes; @@ -121,6 +122,8 @@ import com.android.os.AtomsProto.PackageNotificationPreferences; import com.android.server.UiServiceTestCase; import com.android.server.notification.PermissionHelper.PackagePermission; +import com.google.common.collect.ImmutableList; + import org.json.JSONArray; import org.json.JSONObject; import org.junit.Before; @@ -3523,8 +3526,37 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testUpdateNotificationChannel_fixedPermission() { + List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0)); when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); + PackageInfo pm = new PackageInfo(); + pm.packageName = PKG_O; + pm.applicationInfo = new ApplicationInfo(); + pm.applicationInfo.uid = UID_O; + List<PackageInfo> packages = ImmutableList.of(pm); + when(mPm.getInstalledPackagesAsUser(any(), anyInt())).thenReturn(packages); + mHelper.updateFixedImportance(users); + + assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); + + NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + + NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_NONE); + update.setAllowBubbles(false); + + mHelper.updateNotificationChannel(PKG_O, UID_O, update, true); + assertEquals(IMPORTANCE_HIGH, + mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance()); + assertEquals(false, + mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).canBubble()); + } + + @Test + public void testUpdateNotificationChannel_defaultApp() { + ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); + toAdd.add(new Pair(PKG_O, UID_O)); + mHelper.updateDefaultApps(0, null, toAdd); NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); @@ -3595,6 +3627,58 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testUpdateFixedImportance_multiUser() { + NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); + NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); + NotificationChannel c = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); + // different uids, same package + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false); + mHelper.createNotificationChannel(PKG_O, UserHandle.PER_USER_RANGE + 1, c, true, true); + + UserInfo user = new UserInfo(); + user.id = 0; + List<UserInfo> users = ImmutableList.of(user); + when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); + PackageInfo pm = new PackageInfo(); + pm.packageName = PKG_O; + pm.applicationInfo = new ApplicationInfo(); + pm.applicationInfo.uid = UID_O; + List<PackageInfo> packages = ImmutableList.of(pm); + when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + mHelper.updateFixedImportance(users); + + assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) + .isImportanceLockedByCriticalDeviceFunction()); + assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, b.getId(), false) + .isImportanceLockedByCriticalDeviceFunction()); + assertFalse(mHelper.getNotificationChannel( + PKG_O, UserHandle.PER_USER_RANGE + 1, c.getId(), false) + .isImportanceLockedByCriticalDeviceFunction()); + } + + @Test + public void testUpdateFixedImportance_channelDoesNotExistYet() { + UserInfo user = new UserInfo(); + user.id = 0; + List<UserInfo> users = ImmutableList.of(user); + when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); + PackageInfo pm = new PackageInfo(); + pm.packageName = PKG_O; + pm.applicationInfo = new ApplicationInfo(); + pm.applicationInfo.uid = UID_O; + List<PackageInfo> packages = ImmutableList.of(pm); + when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + mHelper.updateFixedImportance(users); + + NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + + assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) + .isImportanceLockedByCriticalDeviceFunction()); + } + + @Test public void testUpdateDefaultApps_add_multiUser() { NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW); @@ -3759,6 +3843,62 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testUpdateFixedImportance_thenDefaultAppsRemoves() { + UserInfo user = new UserInfo(); + user.id = 0; + List<UserInfo> users = ImmutableList.of(user); + when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); + PackageInfo pm = new PackageInfo(); + pm.packageName = PKG_O; + pm.applicationInfo = new ApplicationInfo(); + pm.applicationInfo.uid = UID_O; + List<PackageInfo> packages = ImmutableList.of(pm); + when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + mHelper.updateFixedImportance(users); + + ArraySet<String> toRemove = new ArraySet<>(); + toRemove.add(PKG_O); + mHelper.updateDefaultApps(0, toRemove, null); + + assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); + + NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + + // Still locked by permission if not role + assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) + .isImportanceLockedByCriticalDeviceFunction()); + } + + @Test + public void testUpdateDefaultApps_thenNotFixedPermission() { + ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>(); + toAdd.add(new Pair(PKG_O, UID_O)); + mHelper.updateDefaultApps(0, null, toAdd); + + UserInfo user = new UserInfo(); + user.id = 0; + List<UserInfo> users = ImmutableList.of(user); + when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(false); + PackageInfo pm = new PackageInfo(); + pm.packageName = PKG_O; + pm.applicationInfo = new ApplicationInfo(); + pm.applicationInfo.uid = UID_O; + List<PackageInfo> packages = ImmutableList.of(pm); + when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages); + mHelper.updateFixedImportance(users); + + assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O)); + + NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false); + + // Still locked by role if not permission + assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false) + .isImportanceLockedByCriticalDeviceFunction()); + } + + @Test public void testChannelXml_backupDefaultApp() throws Exception { NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 533540e2568d..a34896a419ed 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -611,10 +611,9 @@ public class ActivityRecordTests extends WindowTestsBase { activity.setRequestedOrientation(activityCurOrientation == ORIENTATION_LANDSCAPE ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE); - // Asserts fixed orientation request is ignored, and the orientation is not changed - // (fill Task). - assertEquals(activityCurOrientation, activity.getConfiguration().orientation); - assertFalse(activity.isLetterboxedForFixedOrientationAndAspectRatio()); + // Asserts fixed orientation request is not ignored, and the orientation is changed. + assertNotEquals(activityCurOrientation, activity.getConfiguration().orientation); + assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio()); } @Test @@ -2854,6 +2853,11 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(activity2.isResizeable()); activity1.reparent(taskFragment1, POSITION_TOP); + // Adds an Activity which doesn't have shared starting data, and verify if it blocks + // starting window removal. + final ActivityRecord activity3 = new ActivityBuilder(mAtm).build(); + taskFragment2.addChild(activity3, POSITION_TOP); + verify(activity1.getSyncTransaction()).reparent(eq(startingWindow.mSurfaceControl), eq(task.mSurfaceControl)); assertEquals(activity1.mStartingData, startingWindow.mStartingData); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 8474a36dc681..77f884c93682 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -1128,6 +1128,41 @@ public class AppTransitionControllerTest extends WindowTestsBase { verify(activity).setDropInputMode(DropInputMode.NONE); } + /** + * We don't need to drop input for fully trusted embedding (system app, and embedding in the + * same app). This will allow users to do fast tapping. + */ + @Test + public void testOverrideTaskFragmentAdapter_noInputProtectedForFullyTrustedAnimation() { + final Task task = createTask(mDisplayContent); + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner(); + setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner); + + // Create a TaskFragment with only trusted embedded activity + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .createActivityCount(1) + .setOrganizer(organizer) + .build(); + final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord(); + prepareActivityForAppTransition(activity); + final int uid = mAtm.mTaskFragmentOrganizerController.getTaskFragmentOrganizerUid( + getITaskFragmentOrganizer(organizer)); + doReturn(true).when(task).isFullyTrustedEmbedding(uid); + spyOn(mDisplayContent.mAppTransition); + + // Prepare and start transition. + prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment); + mWm.mAnimator.executeAfterPrepareSurfacesRunnables(); + + // The animation will be animated remotely by client, but input should not be dropped for + // fully trusted. + assertTrue(remoteAnimationRunner.isAnimationStarted()); + verify(activity, never()).setDropInputForAnimation(true); + verify(activity, never()).setDropInputMode(DropInputMode.ALL); + } + @Test public void testTransitionGoodToGoForTaskFragments() { final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); @@ -1197,8 +1232,7 @@ public class AppTransitionControllerTest extends WindowTestsBase { TestRemoteAnimationRunner remoteAnimationRunner) { final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( remoteAnimationRunner, 10, 1); - final ITaskFragmentOrganizer iOrganizer = - ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); + final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer); final RemoteAnimationDefinition definition = new RemoteAnimationDefinition(); definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter); definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter); @@ -1208,6 +1242,11 @@ public class AppTransitionControllerTest extends WindowTestsBase { definition); } + private static ITaskFragmentOrganizer getITaskFragmentOrganizer( + TaskFragmentOrganizer organizer) { + return ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); + } + private void prepareAndTriggerAppTransition(@Nullable ActivityRecord openingActivity, @Nullable ActivityRecord closingActivity, @Nullable TaskFragment changingTaskFragment) { if (openingActivity != null) { diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index 3beb7f2049df..55a7c1ba0bca 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -64,6 +64,11 @@ public class DimmerTests extends WindowTestsBase { } @Override + public SurfaceControl.Transaction getSyncTransaction() { + return mTransaction; + } + + @Override public SurfaceControl.Transaction getPendingTransaction() { return mTransaction; } @@ -102,6 +107,11 @@ public class DimmerTests extends WindowTestsBase { } @Override + public SurfaceControl.Transaction getSyncTransaction() { + return mHostTransaction; + } + + @Override public SurfaceControl.Transaction getPendingTransaction() { return mHostTransaction; } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java index 6d022262b720..ffa21fadff6b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java @@ -449,6 +449,36 @@ public class InsetsStateControllerTest extends WindowTestsBase { assertNotNull(app.getInsetsState().peekSource(ITYPE_NAVIGATION_BAR)); } + @UseTestDisplay(addWindows = W_INPUT_METHOD) + @Test + public void testGetInsetsHintForNewControl() { + final WindowState app1 = createTestWindow("app1"); + final WindowState app2 = createTestWindow("app2"); + + makeWindowVisible(mImeWindow); + final InsetsSourceProvider imeInsetsProvider = getController().getSourceProvider(ITYPE_IME); + imeInsetsProvider.setWindowContainer(mImeWindow, null, null); + imeInsetsProvider.updateSourceFrame(mImeWindow.getFrame()); + + imeInsetsProvider.updateControlForTarget(app1, false); + imeInsetsProvider.onPostLayout(); + final InsetsSourceControl control1 = imeInsetsProvider.getControl(app1); + assertNotNull(control1); + assertEquals(imeInsetsProvider.getSource().getFrame().height(), + control1.getInsetsHint().bottom); + + // Simulate the IME control target updated from app1 to app2 when IME insets was invisible. + imeInsetsProvider.setServerVisible(false); + imeInsetsProvider.updateControlForTarget(app2, false); + + // Verify insetsHint of the new control is same as last IME source frame after the layout. + imeInsetsProvider.onPostLayout(); + final InsetsSourceControl control2 = imeInsetsProvider.getControl(app2); + assertNotNull(control2); + assertEquals(imeInsetsProvider.getSource().getFrame().height(), + control2.getInsetsHint().bottom); + } + private WindowState createTestWindow(String name) { final WindowState win = createWindow(null, TYPE_APPLICATION, name); win.setHasSurface(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java index 9fc9489e3c2e..1d14dc31fa26 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java @@ -82,6 +82,7 @@ import android.util.Pair; import androidx.test.filters.SmallTest; import com.android.internal.statusbar.IStatusBarService; +import com.android.internal.telephony.CellBroadcastUtils; import com.android.internal.widget.LockPatternUtils; import com.android.server.LocalServices; import com.android.server.statusbar.StatusBarManagerInternal; @@ -297,6 +298,22 @@ public class LockTaskControllerTest { } @Test + public void testLockTaskViolation_wirelessEmergencyAlerts() { + // GIVEN one task record with allowlisted auth that is in lock task mode + Task tr = getTask(LOCK_TASK_AUTH_ALLOWLISTED); + mLockTaskController.startLockTaskMode(tr, false, TEST_UID); + + // GIVEN cellbroadcast task necessary for emergency warning alerts + Task cellbroadcastreceiver = getTask( + new Intent().setComponent( + CellBroadcastUtils.getDefaultCellBroadcastAlertDialogComponent(mContext)), + LOCK_TASK_AUTH_PINNABLE); + + // THEN the cellbroadcast task should all be allowed + assertFalse(mLockTaskController.isLockTaskModeViolation(cellbroadcastreceiver)); + } + + @Test public void testStopLockTaskMode() throws Exception { // GIVEN one task record with allowlisted auth that is in lock task mode Task tr = getTask(LOCK_TASK_AUTH_ALLOWLISTED); diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java index 021568dd97b4..eba276f99225 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java @@ -61,6 +61,7 @@ import android.platform.test.annotations.Presubmit; import android.util.SparseBooleanArray; import android.view.IRecentsAnimationRunner; import android.view.SurfaceControl; +import android.view.WindowManager.LayoutParams; import android.window.TaskSnapshot; import androidx.test.filters.SmallTest; @@ -162,6 +163,30 @@ public class RecentsAnimationControllerTest extends WindowTestsBase { } @Test + public void testLaunchAndStartRecents_expectTargetAndVisible() throws Exception { + mWm.setRecentsAnimationController(mController); + final ActivityRecord homeActivity = createHomeActivity(); + final Task task = createTask(mDefaultDisplay); + // Emulate that activity1 has just launched activity2, but app transition has not yet been + // executed. + final ActivityRecord activity1 = createActivityRecord(task); + activity1.setVisible(true); + activity1.mVisibleRequested = false; + activity1.addWindow(createWindowState(new LayoutParams(TYPE_BASE_APPLICATION), activity1)); + + final ActivityRecord activity2 = createActivityRecord(task); + activity2.setVisible(false); + activity2.mVisibleRequested = true; + + mDefaultDisplay.getConfiguration().windowConfiguration.setRotation( + mDefaultDisplay.getRotation()); + initializeRecentsAnimationController(mController, homeActivity); + mController.startAnimation(); + verify(mMockRunner, never()).onAnimationCanceled(null /* taskIds */, + null /* taskSnapshots */); + } + + @Test public void testWallpaperIncluded_expectTarget() throws Exception { mWm.setRecentsAnimationController(mController); final ActivityRecord homeActivity = createHomeActivity(); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 76fb7ff2e6f8..35d8129eb385 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -1876,6 +1876,28 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testResizableFixedOrientationAppInSplitScreen_letterboxForDifferentOrientation() { + setUpDisplaySizeWithApp(1000, 2800); + final TestSplitOrganizer organizer = + new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); + + // Resizable landscape-only activity. + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_LANDSCAPE, /* isUnresizable= */ false); + + final Rect originalBounds = new Rect(mActivity.getBounds()); + + // Move activity to split screen which takes half of the screen. + mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test"); + organizer.mPrimary.setBounds(0, 0, 1000, 1400); + assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode()); + assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); + + // Resizable activity is not in size compat mode but in the letterbox for fixed orientation. + assertFitted(); + assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio()); + } + + @Test public void testSupportsNonResizableInSplitScreen_fillTaskForSameOrientation() { // Support non resizable in multi window mAtm.mDevEnableNonResizableMultiWindow = true; diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java index 79ba1759f4c4..ff0063c4ffa0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java @@ -338,6 +338,11 @@ public class SurfaceAnimatorTest extends WindowTestsBase { } @Override + public SurfaceControl.Transaction getSyncTransaction() { + return mTransaction; + } + + @Override public Transaction getPendingTransaction() { return mTransaction; } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 75b5e73a604e..5340a79f4b94 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -511,11 +511,12 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testApplyTransaction_reparentActivityToTaskFragment_triggerLifecycleUpdate() throws RemoteException { - final ActivityRecord activity = createActivityRecord(mDefaultDisplay); + final Task task = createTask(mDisplayContent); + final ActivityRecord activity = createActivityRecord(task); mOrganizer.applyTransaction(mTransaction); mController.registerOrganizer(mIOrganizer); mTaskFragment = new TaskFragmentBuilder(mAtm) - .setCreateParentTask() + .setParentTask(task) .setFragmentToken(mFragmentToken) .build(); mAtm.mWindowOrganizerController.mLaunchTaskFragments diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java index 2893f807c8a8..dc96c66bff29 100644 --- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java @@ -89,38 +89,36 @@ public final class UsbDirectMidiDevice implements Closeable { private final Object mLock = new Object(); private boolean mIsOpen; + private boolean mServerAvailable; private UsbMidiPacketConverter mUsbMidiPacketConverter; private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() { - @Override public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { MidiDeviceInfo deviceInfo = status.getDeviceInfo(); int numInputPorts = deviceInfo.getInputPortCount(); int numOutputPorts = deviceInfo.getOutputPortCount(); - boolean hasOpenPorts = false; + int numOpenPorts = 0; for (int i = 0; i < numInputPorts; i++) { if (status.isInputPortOpen(i)) { - hasOpenPorts = true; - break; + numOpenPorts++; } } - if (!hasOpenPorts) { - for (int i = 0; i < numOutputPorts; i++) { - if (status.getOutputPortOpenCount(i) > 0) { - hasOpenPorts = true; - break; - } + for (int i = 0; i < numOutputPorts; i++) { + if (status.getOutputPortOpenCount(i) > 0) { + numOpenPorts += status.getOutputPortOpenCount(i); } } synchronized (mLock) { - if (hasOpenPorts && !mIsOpen) { + Log.d(TAG, "numOpenPorts: " + numOpenPorts + " isOpen: " + mIsOpen + + " mServerAvailable: " + mServerAvailable); + if ((numOpenPorts > 0) && !mIsOpen && mServerAvailable) { openLocked(); - } else if (!hasOpenPorts && mIsOpen) { + } else if ((numOpenPorts == 0) && mIsOpen) { closeLocked(); } } @@ -348,7 +346,7 @@ public final class UsbDirectMidiDevice implements Closeable { final UsbRequest response = connectionFinal.requestWait(); if (response != request) { Log.w(TAG, "Unexpected response"); - continue; + break; } int bytesRead = byteBuffer.position(); @@ -462,7 +460,7 @@ public final class UsbDirectMidiDevice implements Closeable { mContext = context; MidiManager midiManager = context.getSystemService(MidiManager.class); if (midiManager == null) { - Log.e(TAG, "No MidiManager in UsbDirectMidiDevice.create()"); + Log.e(TAG, "No MidiManager in UsbDirectMidiDevice.register()"); return false; } @@ -499,6 +497,7 @@ public final class UsbDirectMidiDevice implements Closeable { mUsbDevice.getSerialNumber()); properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, mUsbDevice); + mServerAvailable = true; mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs, null, null, properties, MidiDeviceInfo.TYPE_USB, mDefaultMidiProtocol, mCallback); if (mServer == null) { @@ -514,6 +513,7 @@ public final class UsbDirectMidiDevice implements Closeable { if (mIsOpen) { closeLocked(); } + mServerAvailable = false; } if (mServer != null) { diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java index 275f21755141..67955e155f19 100644 --- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java +++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java @@ -71,40 +71,38 @@ public final class UsbMidiDevice implements Closeable { private final Object mLock = new Object(); private boolean mIsOpen; + private boolean mServerAvailable; // pipe file descriptor for signalling input thread to exit // only accessed from JNI code private int mPipeFD = -1; private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() { - @Override public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) { MidiDeviceInfo deviceInfo = status.getDeviceInfo(); - int inputPorts = deviceInfo.getInputPortCount(); - int outputPorts = deviceInfo.getOutputPortCount(); - boolean hasOpenPorts = false; + int numInputPorts = deviceInfo.getInputPortCount(); + int numOutputPorts = deviceInfo.getOutputPortCount(); + int numOpenPorts = 0; - for (int i = 0; i < inputPorts; i++) { + for (int i = 0; i < numInputPorts; i++) { if (status.isInputPortOpen(i)) { - hasOpenPorts = true; - break; + numOpenPorts++; } } - if (!hasOpenPorts) { - for (int i = 0; i < outputPorts; i++) { - if (status.getOutputPortOpenCount(i) > 0) { - hasOpenPorts = true; - break; - } + for (int i = 0; i < numOutputPorts; i++) { + if (status.getOutputPortOpenCount(i) > 0) { + numOpenPorts += status.getOutputPortOpenCount(i); } } synchronized (mLock) { - if (hasOpenPorts && !mIsOpen) { + Log.d(TAG, "numOpenPorts: " + numOpenPorts + " isOpen: " + mIsOpen + + " mServerAvailable: " + mServerAvailable); + if ((numOpenPorts > 0) && !mIsOpen && mServerAvailable) { openLocked(); - } else if (!hasOpenPorts && mIsOpen) { + } else if ((numOpenPorts == 0) && mIsOpen) { closeLocked(); } } @@ -298,10 +296,11 @@ public final class UsbMidiDevice implements Closeable { private boolean register(Context context, Bundle properties) { MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE); if (midiManager == null) { - Log.e(TAG, "No MidiManager in UsbMidiDevice.create()"); + Log.e(TAG, "No MidiManager in UsbMidiDevice.register()"); return false; } + mServerAvailable = true; mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs, null, null, properties, MidiDeviceInfo.TYPE_USB, MidiDeviceInfo.PROTOCOL_UNKNOWN, mCallback); @@ -318,6 +317,7 @@ public final class UsbMidiDevice implements Closeable { if (mIsOpen) { closeLocked(); } + mServerAvailable = false; } if (mServer != null) { diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 2cb398204743..22e0d0831dc7 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -124,6 +124,10 @@ final class HotwordDetectionConnection { private static final long RESET_DEBUG_HOTWORD_LOGGING_TIMEOUT_MILLIS = 60 * 60 * 1000; // 1 hour private static final int MAX_ISOLATED_PROCESS_NUMBER = 10; + // The error codes are used for onError callback + private static final int HOTWORD_DETECTION_SERVICE_DIED = -1; + private static final int CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION = -2; + // Hotword metrics private static final int METRICS_INIT_UNKNOWN_TIMEOUT = HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_TIMEOUT; @@ -420,19 +424,24 @@ final class HotwordDetectionConnection { Slog.d(TAG, "onDetected"); } synchronized (mLock) { - if (mPerformingSoftwareHotwordDetection) { + if (!mPerformingSoftwareHotwordDetection) { + Slog.i(TAG, "Hotword detection has already completed"); + return; + } + mPerformingSoftwareHotwordDetection = false; + try { enforcePermissionsForDataDelivery(); - mSoftwareCallback.onDetected(result, null, null); - mPerformingSoftwareHotwordDetection = false; - if (result != null) { - Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result) - + " bits from hotword trusted process"); - if (mDebugHotwordLogging) { - Slog.i(TAG, "Egressed detected result: " + result); - } + } catch (SecurityException e) { + mSoftwareCallback.onError(); + return; + } + mSoftwareCallback.onDetected(result, null, null); + if (result != null) { + Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result) + + " bits from hotword trusted process"); + if (mDebugHotwordLogging) { + Slog.i(TAG, "Egressed detected result: " + result); } - } else { - Slog.i(TAG, "Hotword detection has already completed"); } } } @@ -513,19 +522,24 @@ final class HotwordDetectionConnection { public void onDetected(HotwordDetectedResult result) throws RemoteException { Slog.v(TAG, "onDetected"); synchronized (mLock) { - if (mValidatingDspTrigger) { - mValidatingDspTrigger = false; + if (!mValidatingDspTrigger) { + Slog.i(TAG, "Ignored hotword detected since trigger has been handled"); + return; + } + mValidatingDspTrigger = false; + try { enforcePermissionsForDataDelivery(); - externalCallback.onKeyphraseDetected(recognitionEvent, result); - if (result != null) { - Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result) - + " bits from hotword trusted process"); - if (mDebugHotwordLogging) { - Slog.i(TAG, "Egressed detected result: " + result); - } + } catch (SecurityException e) { + externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION); + return; + } + externalCallback.onKeyphraseDetected(recognitionEvent, result); + if (result != null) { + Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result) + + " bits from hotword trusted process"); + if (mDebugHotwordLogging) { + Slog.i(TAG, "Egressed detected result: " + result); } - } else { - Slog.i(TAG, "Ignored hotword detected since trigger has been handled"); } } } @@ -595,7 +609,8 @@ final class HotwordDetectionConnection { HotwordMetricsLogger.writeKeyphraseTriggerEvent( mDetectorType, METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION); - throw e; + externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION); + return; } externalCallback.onKeyphraseDetected(recognitionEvent, result); if (result != null) { @@ -885,7 +900,13 @@ final class HotwordDetectionConnection { throws RemoteException { bestEffortClose(serviceAudioSink); bestEffortClose(serviceAudioSource); - enforcePermissionsForDataDelivery(); + try { + enforcePermissionsForDataDelivery(); + } catch (SecurityException e) { + bestEffortClose(audioSource); + callback.onError(); + return; + } callback.onDetected(triggerResult, null /* audioFormat */, null /* audioStream */); if (triggerResult != null) { @@ -984,7 +1005,7 @@ final class HotwordDetectionConnection { Slog.w(TAG, "binderDied"); try { - mCallback.onError(-1); + mCallback.onError(HOTWORD_DETECTION_SERVICE_DIED); } catch (RemoteException e) { Slog.w(TAG, "Failed to report onError status: " + e); } diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index 980ea5cd7f8c..432af3aa1aa0 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -2895,7 +2895,19 @@ public final class Call { if (key != null) { final Object value = bundle.get(key); final Object newValue = newBundle.get(key); - if (!Objects.equals(value, newValue)) { + if (!newBundle.containsKey(key)) { + return false; + } + if (value instanceof Bundle && newValue instanceof Bundle) { + if (!areBundlesEqual((Bundle) value, (Bundle) newValue)) { + return false; + } + } + if (value instanceof byte[] && newValue instanceof byte[]) { + if (!Arrays.equals((byte[]) value, (byte[]) newValue)) { + return false; + } + } else if (!Objects.equals(value, newValue)) { return false; } } diff --git a/telephony/common/android/telephony/LocationAccessPolicy.java b/telephony/common/android/telephony/LocationAccessPolicy.java index 85d59a216f25..d4b6c91eb7a0 100644 --- a/telephony/common/android/telephony/LocationAccessPolicy.java +++ b/telephony/common/android/telephony/LocationAccessPolicy.java @@ -316,9 +316,11 @@ public final class LocationAccessPolicy { return LocationPermissionResult.ALLOWED; } - // Check the system-wide requirements. If the location main switch is off or - // the app's profile isn't in foreground, return a soft denial. - if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid)) { + // Check the system-wide requirements. If the location main switch is off and the caller is + // not in the allowlist of apps that always have loation access or the app's profile + // isn't in the foreground, return a soft denial. + if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid, + query.callingPackage)) { return LocationPermissionResult.DENIED_SOFT; } @@ -344,15 +346,16 @@ public final class LocationAccessPolicy { return LocationPermissionResult.ALLOWED; } - private static boolean checkManifestPermission(Context context, int pid, int uid, String permissionToCheck) { return context.checkPermission(permissionToCheck, pid, uid) == PackageManager.PERMISSION_GRANTED; } - private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid) { - if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())) { + private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid, + @NonNull String callingPackage) { + if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier()) + && !isLocationBypassAllowed(context, callingPackage)) { if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")"); return false; } @@ -361,7 +364,10 @@ public final class LocationAccessPolicy { return isCurrentProfile(context, uid) || checkInteractAcrossUsersFull(context, pid, uid); } - private static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) { + /** + * @return Whether location is enabled for the given user. + */ + public static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) { LocationManager locationManager = context.getSystemService(LocationManager.class); if (locationManager == null) { Log.w(TAG, "Couldn't get location manager, denying location access"); @@ -370,6 +376,24 @@ public final class LocationAccessPolicy { return locationManager.isLocationEnabledForUser(UserHandle.of(userId)); } + private static boolean isLocationBypassAllowed(@NonNull Context context, + @NonNull String callingPackage) { + for (String bypassPackage : getLocationBypassPackages(context)) { + if (callingPackage.equals(bypassPackage)) { + return true; + } + } + return false; + } + + /** + * @return An array of packages that are always allowed to access location. + */ + public static @NonNull String[] getLocationBypassPackages(@NonNull Context context) { + return context.getResources().getStringArray( + com.android.internal.R.array.config_serviceStateLocationAllowedPackages); + } + private static boolean checkInteractAcrossUsersFull( @NonNull Context context, int pid, int uid) { return checkManifestPermission(context, pid, uid, diff --git a/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java b/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java index 6c6375586225..6181329bcb2a 100644 --- a/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java +++ b/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java @@ -16,6 +16,7 @@ package com.android.internal.telephony; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -62,4 +63,17 @@ public class CellBroadcastUtils { return packageName; } + + /** + * Utility method to get cellbroadcast alert dialog component name + */ + public static ComponentName getDefaultCellBroadcastAlertDialogComponent(Context context) { + String cellBroadcastReceiverPackageName = + getDefaultCellBroadcastReceiverPackageName(context); + if (TextUtils.isEmpty(cellBroadcastReceiverPackageName)) { + return null; + } + return ComponentName.createRelative(cellBroadcastReceiverPackageName, + "com.android.cellbroadcastreceiver.CellBroadcastAlertDialog"); + } } diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java index 78b0b844865d..4924a82c385f 100644 --- a/telephony/common/com/android/internal/telephony/SmsApplication.java +++ b/telephony/common/com/android/internal/telephony/SmsApplication.java @@ -68,7 +68,6 @@ import java.util.stream.Collectors; public final class SmsApplication { static final String LOG_TAG = "SmsApplication"; public static final String PHONE_PACKAGE_NAME = "com.android.phone"; - public static final String BLUETOOTH_PACKAGE_NAME = "com.android.bluetooth.services"; public static final String MMS_SERVICE_PACKAGE_NAME = "com.android.mms.service"; public static final String TELEPHONY_PROVIDER_PACKAGE_NAME = "com.android.providers.telephony"; @@ -541,11 +540,13 @@ public final class SmsApplication { PackageManager packageManager = context.getPackageManager(); AppOpsManager appOps = context.getSystemService(AppOpsManager.class); + final String bluetoothPackageName = context.getResources() + .getString(com.android.internal.R.string.config_systemBluetoothStack); // Assign permission to special system apps assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps, PHONE_PACKAGE_NAME, true); assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps, - BLUETOOTH_PACKAGE_NAME, true); + bluetoothPackageName, false); assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps, MMS_SERVICE_PACKAGE_NAME, true); assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps, @@ -1128,8 +1129,11 @@ public final class SmsApplication { return false; } final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context); + final String bluetoothPackageName = context.getResources() + .getString(com.android.internal.R.string.config_systemBluetoothStack); + if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName)) - || BLUETOOTH_PACKAGE_NAME.equals(packageName)) { + || bluetoothPackageName.equals(packageName)) { return true; } return false; diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 71ffd6e2ec9f..a6e3bb41b87c 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -9290,6 +9290,7 @@ public class TelephonyManager { try { ITelephony telephony = getITelephony(); if (telephony != null) { + networkTypeBitmask = checkNetworkTypeBitmask(networkTypeBitmask); return telephony.setAllowedNetworkTypesForReason(getSubId(), TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER, networkTypeBitmask); } @@ -9300,6 +9301,20 @@ public class TelephonyManager { } /** + * If {@link #NETWORK_TYPE_BITMASK_LTE_CA} bit is set, convert it to NETWORK_TYPE_BITMASK_LTE. + * + * @param networkTypeBitmask The networkTypeBitmask being checked + * @return The checked/converted networkTypeBitmask + */ + private long checkNetworkTypeBitmask(@NetworkTypeBitMask long networkTypeBitmask) { + if ((networkTypeBitmask & NETWORK_TYPE_BITMASK_LTE_CA) != 0) { + networkTypeBitmask ^= NETWORK_TYPE_BITMASK_LTE_CA; + networkTypeBitmask |= NETWORK_TYPE_BITMASK_LTE; + } + return networkTypeBitmask; + } + + /** * Set the allowed network types of the device. This is for carrier or privileged apps to * enable/disable certain network types on the device. The user preferred network types should * be set through {@link #setPreferredNetworkTypeBitmask}. @@ -9325,6 +9340,7 @@ public class TelephonyManager { try { ITelephony telephony = getITelephony(); if (telephony != null) { + allowedNetworkTypes = checkNetworkTypeBitmask(allowedNetworkTypes); return telephony.setAllowedNetworkTypesForReason(getSubId(), TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_CARRIER, allowedNetworkTypes); } @@ -9410,6 +9426,7 @@ public class TelephonyManager { try { ITelephony telephony = getITelephony(); if (telephony != null) { + allowedNetworkTypes = checkNetworkTypeBitmask(allowedNetworkTypes); telephony.setAllowedNetworkTypesForReason(getSubId(), reason, allowedNetworkTypes); } else { @@ -13727,7 +13744,11 @@ public class TelephonyManager { */ public static final long NETWORK_TYPE_BITMASK_LTE = (1 << (NETWORK_TYPE_LTE -1)); /** + * NOT USED; this bitmask is exposed accidentally, will be deprecated in U. + * If used, will be converted to {@link #NETWORK_TYPE_BITMASK_LTE}. * network type bitmask indicating the support of radio tech LTE CA (carrier aggregation). + * + * @see #NETWORK_TYPE_BITMASK_LTE */ public static final long NETWORK_TYPE_BITMASK_LTE_CA = (1 << (NETWORK_TYPE_LTE_CA -1)); diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index eede9dc474f8..a673807a3f97 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -25,18 +25,17 @@ import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.app.Activity; import android.app.PendingIntent; -import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager; -import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.RemoteException; import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; import android.telephony.TelephonyFrameworkInitializer; import android.telephony.TelephonyManager; import android.telephony.UiccCardInfo; @@ -1209,18 +1208,16 @@ public class EuiccManager { return; } try { - // TODO: Uncomment below compat change code once callers are ported to use - // switchToSubscription with portIndex for disable operation. - // if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID - // && getIEuiccController().isCompatChangeEnabled(mContext.getOpPackageName(), - // SWITCH_WITHOUT_PORT_INDEX_EXCEPTION_ON_DISABLE)) { - // // Apps targeting on Android T and beyond will get exception whenever - // // switchToSubscription without portIndex is called with INVALID_SUBSCRIPTION_ID. - // Log.e(TAG, "switchToSubscription without portIndex is not allowed for" - // + " disable operation"); - // throw new IllegalArgumentException("Must use switchToSubscription with portIndex" - // + " API for disable operation"); - // } + if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID + && getIEuiccController().isCompatChangeEnabled(mContext.getOpPackageName(), + SWITCH_WITHOUT_PORT_INDEX_EXCEPTION_ON_DISABLE)) { + // Apps targeting on Android T and beyond will get exception whenever + // switchToSubscription without portIndex is called with INVALID_SUBSCRIPTION_ID. + Log.e(TAG, "switchToSubscription without portIndex is not allowed for" + + " disable operation"); + throw new IllegalArgumentException("Must use switchToSubscription with portIndex" + + " API for disable operation"); + } getIEuiccController().switchToSubscription(mCardId, subscriptionId, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { diff --git a/telephony/java/android/telephony/ims/feature/RcsFeature.java b/telephony/java/android/telephony/ims/feature/RcsFeature.java index 843827befb65..186241217588 100644 --- a/telephony/java/android/telephony/ims/feature/RcsFeature.java +++ b/telephony/java/android/telephony/ims/feature/RcsFeature.java @@ -118,8 +118,10 @@ public class RcsFeature extends ImsFeature { @Override public void setCapabilityExchangeEventListener( @Nullable ICapabilityExchangeEventListener listener) throws RemoteException { - CapabilityExchangeEventListener listenerWrapper = - new CapabilityExchangeAidlWrapper(listener); + // Set the listener wrapper to null if the listener passed in is null. This will notify + // the RcsFeature to trigger the destruction of active capability exchange interface. + CapabilityExchangeEventListener listenerWrapper = listener != null + ? new CapabilityExchangeAidlWrapper(listener) : null; executeMethodAsync(() -> mReference.setCapabilityExchangeEventListener(listenerWrapper), "setCapabilityExchangeEventListener"); } diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java index 7ee19fb37244..052ce3a902c1 100644 --- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java +++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; @@ -214,7 +215,7 @@ public class SmsApplicationTest { ApplicationInfo bluetoothApplicationInfo = new ApplicationInfo(); bluetoothApplicationInfo.uid = FAKE_BT_UID; bluetoothPackageInfo.applicationInfo = bluetoothApplicationInfo; - when(mPackageManager.getPackageInfo(eq(SmsApplication.BLUETOOTH_PACKAGE_NAME), anyInt())) + when(mPackageManager.getPackageInfo(matches(".*android.bluetooth.services"), anyInt())) .thenReturn(bluetoothPackageInfo); PackageInfo telephonyProviderPackageInfo = new PackageInfo(); diff --git a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java index 294f5c1f4842..9c6d85238b77 100644 --- a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java +++ b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java @@ -268,6 +268,15 @@ public class PersistableBundleUtilsTest { } @Test + public void testToFromDiskStableBytes() throws Exception { + final PersistableBundle testBundle = getTestBundle(); + final PersistableBundle result = + PersistableBundleUtils.fromDiskStableBytes( + PersistableBundleUtils.toDiskStableBytes(testBundle)); + assertTrue(PersistableBundleUtils.isEqual(testBundle, result)); + } + + @Test public void testEquality_identical() throws Exception { final PersistableBundle left = getTestBundle(); final PersistableBundle right = getTestBundle(); |