diff options
555 files changed, 9395 insertions, 5504 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 6f8a189c42b8..c76812111cec 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -100,7 +100,6 @@ aconfig_declarations_group { "framework-jobscheduler-job.flags-aconfig-java", "framework_graphics_flags_java_lib", "hwui_flags_java_lib", - "interaction_jank_monitor_flags_lib", "libcore_exported_aconfig_flags_lib", "libgui_flags_java_lib", "power_flags_lib", @@ -1579,17 +1578,3 @@ java_aconfig_library { aconfig_declarations: "dropbox_flags", defaults: ["framework-minus-apex-aconfig-java-defaults"], } - -// Zero Jank -aconfig_declarations { - name: "interaction_jank_monitor_flags", - package: "com.android.internal.jank", - container: "system", - srcs: ["core/java/com/android/internal/jank/flags.aconfig"], -} - -java_aconfig_library { - name: "interaction_jank_monitor_flags_lib", - aconfig_declarations: "interaction_jank_monitor_flags", - defaults: ["framework-minus-apex-aconfig-java-defaults"], -} diff --git a/api/Android.bp b/api/Android.bp index 341be3d53844..533f9f66434b 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -87,6 +87,7 @@ combined_apis { "framework-permission", "framework-permission-s", "framework-profiling", + "framework-photopicker", "framework-scheduling", "framework-sdkextensions", "framework-statsd", diff --git a/core/api/current.txt b/core/api/current.txt index 4e6dacff290e..ddfd364cc55d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7964,13 +7964,13 @@ package android.app.admin { field public static final String LOCK_TASK_POLICY = "lockTask"; field public static final String PACKAGES_SUSPENDED_POLICY = "packagesSuspended"; field public static final String PACKAGE_UNINSTALL_BLOCKED_POLICY = "packageUninstallBlocked"; - field @FlaggedApi("android.app.admin.flags.policy_engine_migration_v2_enabled") public static final String PASSWORD_COMPLEXITY_POLICY = "passwordComplexity"; + field public static final String PASSWORD_COMPLEXITY_POLICY = "passwordComplexity"; field public static final String PERMISSION_GRANT_POLICY = "permissionGrant"; field public static final String PERSISTENT_PREFERRED_ACTIVITY_POLICY = "persistentPreferredActivity"; field public static final String RESET_PASSWORD_TOKEN_POLICY = "resetPasswordToken"; field public static final String SECURITY_LOGGING_POLICY = "securityLogging"; field public static final String STATUS_BAR_DISABLED_POLICY = "statusBarDisabled"; - field @FlaggedApi("android.app.admin.flags.policy_engine_migration_v2_enabled") public static final String USB_DATA_SIGNALING_POLICY = "usbDataSignaling"; + field public static final String USB_DATA_SIGNALING_POLICY = "usbDataSignaling"; field public static final String USER_CONTROL_DISABLED_PACKAGES_POLICY = "userControlDisabledPackages"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 3637ca763e0a..ba160372f51d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -61,6 +61,7 @@ package android { field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String BIND_DOMAIN_SELECTION_SERVICE = "android.permission.BIND_DOMAIN_SELECTION_SERVICE"; field public static final String BIND_DOMAIN_VERIFICATION_AGENT = "android.permission.BIND_DOMAIN_VERIFICATION_AGENT"; field public static final String BIND_EUICC_SERVICE = "android.permission.BIND_EUICC_SERVICE"; + field @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") public static final String BIND_EXPLICIT_HEALTH_CHECK_SERVICE = "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"; field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE"; field public static final String BIND_FIELD_CLASSIFICATION_SERVICE = "android.permission.BIND_FIELD_CLASSIFICATION_SERVICE"; field public static final String BIND_GBA_SERVICE = "android.permission.BIND_GBA_SERVICE"; @@ -3469,6 +3470,7 @@ package android.companion.virtual { public static interface VirtualDeviceManager.ActivityListener { method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public default void onActivityLaunchBlocked(int, @NonNull android.content.ComponentName, @NonNull android.os.UserHandle, @Nullable android.content.IntentSender); method public void onDisplayEmpty(int); + method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public default void onSecureWindowShown(int, @NonNull android.content.ComponentName, @NonNull android.os.UserHandle); method @Deprecated public void onTopActivityChanged(int, @NonNull android.content.ComponentName); method public default void onTopActivityChanged(int, @NonNull android.content.ComponentName, int); } @@ -4631,6 +4633,7 @@ package android.content.rollback { method public int getCommittedSessionId(); method @NonNull public java.util.List<android.content.rollback.PackageRollbackInfo> getPackages(); method public int getRollbackId(); + method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") public int getRollbackImpactLevel(); method public boolean isStaged(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.rollback.RollbackInfo> CREATOR; diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 009d08245da2..42f761570849 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -597,19 +597,19 @@ package android.app.admin { method @RequiresPermission(android.Manifest.permission.FORCE_DEVICE_POLICY_MANAGER_LOGS) public long forceNetworkLogs(); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void forceRemoveActiveAdmin(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.FORCE_DEVICE_POLICY_MANAGER_LOGS) public long forceSecurityLogs(); - method @FlaggedApi("android.app.admin.flags.device_policy_size_tracking_internal_bug_fix_enabled") @RequiresPermission("android.permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT") public void forceSetMaxPolicyStorageLimit(int); + method @RequiresPermission("android.permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT") public void forceSetMaxPolicyStorageLimit(int); method public void forceUpdateUserSetupComplete(int); method @NonNull public java.util.Set<java.lang.String> getDefaultCrossProfilePackages(); method @Deprecated public int getDeviceOwnerType(@NonNull android.content.ComponentName); method @Nullable public String getDevicePolicyManagementRoleHolderUpdaterPackage(); method @NonNull public java.util.Set<java.lang.String> getDisallowedSystemApps(@NonNull android.content.ComponentName, int, @NonNull String); - method @FlaggedApi("android.app.admin.flags.headless_device_owner_provisioning_fix_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int getHeadlessDeviceOwnerMode(); + method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int getHeadlessDeviceOwnerMode(); method public long getLastBugReportRequestTime(); method public long getLastNetworkLogRetrievalTime(); method public long getLastSecurityLogRetrievalTime(); method public java.util.List<java.lang.String> getOwnerInstalledCaCerts(@NonNull android.os.UserHandle); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public java.util.Set<java.lang.String> getPolicyExemptApps(); - method @FlaggedApi("android.app.admin.flags.device_policy_size_tracking_internal_bug_fix_enabled") @RequiresPermission("android.permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT") public int getPolicySizeForAdmin(@NonNull android.app.admin.EnforcingAdmin); + method @RequiresPermission("android.permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT") public int getPolicySizeForAdmin(@NonNull android.app.admin.EnforcingAdmin); method public boolean isCurrentInputMethodSetByOwner(); method public boolean isFactoryResetProtectionPolicySupported(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean isNewUserDisclaimerAcknowledged(); @@ -680,7 +680,7 @@ package android.app.admin { } public final class EnforcingAdmin implements android.os.Parcelable { - ctor @FlaggedApi("android.app.admin.flags.device_policy_size_tracking_internal_bug_fix_enabled") public EnforcingAdmin(@NonNull String, @NonNull android.app.admin.Authority, @NonNull android.os.UserHandle, @Nullable android.content.ComponentName); + ctor public EnforcingAdmin(@NonNull String, @NonNull android.app.admin.Authority, @NonNull android.os.UserHandle, @Nullable android.content.ComponentName); } public final class FlagUnion extends android.app.admin.ResolutionMechanism<java.lang.Integer> { @@ -1269,10 +1269,6 @@ package android.content.res { package android.content.rollback { - public final class RollbackInfo implements android.os.Parcelable { - method @FlaggedApi("android.content.pm.recoverability_detection") public int getRollbackImpactLevel(); - } - public final class RollbackManager { method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void blockRollbackManager(long); method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void expireRollbackForPackage(@NonNull String); @@ -1801,11 +1797,15 @@ package android.hardware.input { public class InputSettings { method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") public static int getAccessibilityBounceKeysThreshold(@NonNull android.content.Context); + method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") public static int getAccessibilityRepeatKeysDelay(@NonNull android.content.Context); + method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") public static int getAccessibilityRepeatKeysTimeout(@NonNull android.content.Context); method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") public static int getAccessibilitySlowKeysThreshold(@NonNull android.content.Context); method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") public static boolean isAccessibilityMouseKeysEnabled(@NonNull android.content.Context); method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") public static boolean isAccessibilityStickyKeysEnabled(@NonNull android.content.Context); method @FlaggedApi("com.android.hardware.input.keyboard_a11y_bounce_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityBounceKeysThreshold(@NonNull android.content.Context, int); method @FlaggedApi("com.android.hardware.input.keyboard_a11y_mouse_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityMouseKeysEnabled(@NonNull android.content.Context, boolean); + method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityRepeatKeysDelay(@NonNull android.content.Context, int); + method @FlaggedApi("com.android.hardware.input.keyboard_repeat_keys") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityRepeatKeysTimeout(@NonNull android.content.Context, int); method @FlaggedApi("com.android.hardware.input.keyboard_a11y_slow_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilitySlowKeysThreshold(@NonNull android.content.Context, int); method @FlaggedApi("com.android.hardware.input.keyboard_a11y_sticky_keys_flag") @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setAccessibilityStickyKeysEnabled(@NonNull android.content.Context, boolean); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static void setMaximumObscuringOpacityForTouch(@NonNull android.content.Context, @FloatRange(from=0, to=1) float); diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 21396a1a36e5..8fd332621599 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -7459,15 +7459,15 @@ public class AppOpsManager { } /** - * Similar to {@link #onOpChanged(String, String, int)} but includes the device for which - * the op mode has changed. + * Similar to {@link #onOpChanged(String, String)} but includes user and the device for + * which the op mode has changed. * * <p> Implement this method if callbacks are required on all devices. * If not implemented explicitly, the default implementation will notify for op changes - * on the default device {@link VirtualDeviceManager#PERSISTENT_DEVICE_ID_DEFAULT} only. + * on the default device only. * - * <p> If implemented, {@link #onOpChanged(String, String, int)} - * will not be called automatically. + * <p> If implemented, {@link #onOpChanged(String, String)} will not be called + * automatically. * * @param op The Op that changed. * @param packageName Package of the app whose Op changed. diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 8b3ee24db025..e44e7768724e 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -31,6 +31,7 @@ import android.app.admin.IDevicePolicyManager; import android.app.ambientcontext.AmbientContextManager; import android.app.ambientcontext.IAmbientContextManager; import android.app.appfunctions.AppFunctionManager; +import android.app.appfunctions.AppFunctionManagerConfiguration; import android.app.appfunctions.IAppFunctionManager; import android.app.appsearch.AppSearchManagerFrameworkInitializer; import android.app.blob.BlobStoreManagerFrameworkInitializer; @@ -937,8 +938,10 @@ public final class SystemServiceRegistry { @Override public AppFunctionManager createService(ContextImpl ctx) throws ServiceNotFoundException { + if (!AppFunctionManagerConfiguration.isSupported(ctx)) { + return null; + } IAppFunctionManager service; - //TODO(b/357551503): If the feature not present avoid look up every time service = IAppFunctionManager.Stub.asInterface( ServiceManager.getServiceOrThrow(Context.APP_FUNCTION_SERVICE)); return new AppFunctionManager(service, ctx.getOuterContext()); diff --git a/core/java/android/app/admin/AccountTypePolicyKey.java b/core/java/android/app/admin/AccountTypePolicyKey.java index 02e492bb06aa..515c1c66b2a3 100644 --- a/core/java/android/app/admin/AccountTypePolicyKey.java +++ b/core/java/android/app/admin/AccountTypePolicyKey.java @@ -24,7 +24,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.admin.flags.Flags; import android.os.Bundle; import android.os.Parcel; @@ -54,9 +53,7 @@ public final class AccountTypePolicyKey extends PolicyKey { @TestApi public AccountTypePolicyKey(@NonNull String key, @NonNull String accountType) { super(key); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxStringLength(accountType, "accountType"); - } + PolicySizeVerifier.enforceMaxStringLength(accountType, "accountType"); mAccountType = Objects.requireNonNull((accountType)); } diff --git a/core/java/android/app/admin/BundlePolicyValue.java b/core/java/android/app/admin/BundlePolicyValue.java index c993671f4fc1..00e67e64502a 100644 --- a/core/java/android/app/admin/BundlePolicyValue.java +++ b/core/java/android/app/admin/BundlePolicyValue.java @@ -18,7 +18,6 @@ package android.app.admin; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.admin.flags.Flags; import android.os.Bundle; import android.os.Parcel; @@ -31,9 +30,7 @@ public final class BundlePolicyValue extends PolicyValue<Bundle> { public BundlePolicyValue(Bundle value) { super(value); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxBundleFieldsLength(value); - } + PolicySizeVerifier.enforceMaxBundleFieldsLength(value); } private BundlePolicyValue(Parcel source) { diff --git a/core/java/android/app/admin/ComponentNamePolicyValue.java b/core/java/android/app/admin/ComponentNamePolicyValue.java index a7a2f7d27e0d..f092b7bb5538 100644 --- a/core/java/android/app/admin/ComponentNamePolicyValue.java +++ b/core/java/android/app/admin/ComponentNamePolicyValue.java @@ -18,7 +18,6 @@ package android.app.admin; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.admin.flags.Flags; import android.content.ComponentName; import android.os.Parcel; @@ -31,9 +30,7 @@ public final class ComponentNamePolicyValue extends PolicyValue<ComponentName> { public ComponentNamePolicyValue(@NonNull ComponentName value) { super(value); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxComponentNameLength(value); - } + PolicySizeVerifier.enforceMaxComponentNameLength(value); } private ComponentNamePolicyValue(Parcel source) { diff --git a/core/java/android/app/admin/DevicePolicyIdentifiers.java b/core/java/android/app/admin/DevicePolicyIdentifiers.java index 156512a90295..c0e435c04d3c 100644 --- a/core/java/android/app/admin/DevicePolicyIdentifiers.java +++ b/core/java/android/app/admin/DevicePolicyIdentifiers.java @@ -16,8 +16,6 @@ package android.app.admin; -import static android.app.admin.flags.Flags.FLAG_POLICY_ENGINE_MIGRATION_V2_ENABLED; - import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; @@ -185,13 +183,11 @@ public final class DevicePolicyIdentifiers { /** * String identifier for {@link DevicePolicyManager#setUsbDataSignalingEnabled}. */ - @FlaggedApi(FLAG_POLICY_ENGINE_MIGRATION_V2_ENABLED) public static final String USB_DATA_SIGNALING_POLICY = "usbDataSignaling"; /** * String identifier for {@link DevicePolicyManager#setRequiredPasswordComplexity}. */ - @FlaggedApi(FLAG_POLICY_ENGINE_MIGRATION_V2_ENABLED) public static final String PASSWORD_COMPLEXITY_POLICY = "passwordComplexity"; /** diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index d31d8f27844a..1ddec17e49bb 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -54,10 +54,8 @@ import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY; import static android.Manifest.permission.SET_TIME; import static android.Manifest.permission.SET_TIME_ZONE; import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; -import static android.app.admin.flags.Flags.FLAG_DEVICE_POLICY_SIZE_TRACKING_INTERNAL_BUG_FIX_ENABLED; import static android.app.admin.flags.Flags.FLAG_DEVICE_THEFT_API_ENABLED; import static android.app.admin.flags.Flags.FLAG_DEVICE_POLICY_SIZE_TRACKING_ENABLED; -import static android.app.admin.flags.Flags.FLAG_HEADLESS_DEVICE_OWNER_PROVISIONING_FIX_ENABLED; import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static android.app.admin.flags.Flags.onboardingConsentlessBugreports; import static android.app.admin.flags.Flags.FLAG_IS_MTE_POLICY_ENFORCED; @@ -17809,7 +17807,6 @@ public class DevicePolicyManager { */ @TestApi @RequiresPermission(permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT) - @FlaggedApi(FLAG_DEVICE_POLICY_SIZE_TRACKING_INTERNAL_BUG_FIX_ENABLED) public void forceSetMaxPolicyStorageLimit(int storageLimit) { if (mService != null) { try { @@ -17827,7 +17824,6 @@ public class DevicePolicyManager { */ @TestApi @RequiresPermission(permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT) - @FlaggedApi(FLAG_DEVICE_POLICY_SIZE_TRACKING_INTERNAL_BUG_FIX_ENABLED) public int getPolicySizeForAdmin(@NonNull EnforcingAdmin admin) { if (mService != null) { try { @@ -17846,13 +17842,9 @@ public class DevicePolicyManager { * @hide */ @TestApi - @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_PROVISIONING_FIX_ENABLED) @RequiresPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) @DeviceAdminInfo.HeadlessDeviceOwnerMode public int getHeadlessDeviceOwnerMode() { - if (!Flags.headlessDeviceOwnerProvisioningFixEnabled()) { - return HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; - } if (mService != null) { try { return mService.getHeadlessDeviceOwnerMode(mContext.getPackageName()); diff --git a/core/java/android/app/admin/EnforcingAdmin.java b/core/java/android/app/admin/EnforcingAdmin.java index f70a53f61671..5f9bb9c22893 100644 --- a/core/java/android/app/admin/EnforcingAdmin.java +++ b/core/java/android/app/admin/EnforcingAdmin.java @@ -16,9 +16,6 @@ package android.app.admin; -import static android.app.admin.flags.Flags.FLAG_DEVICE_POLICY_SIZE_TRACKING_INTERNAL_BUG_FIX_ENABLED; - -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -64,7 +61,6 @@ public final class EnforcingAdmin implements Parcelable { * * @hide */ - @FlaggedApi(FLAG_DEVICE_POLICY_SIZE_TRACKING_INTERNAL_BUG_FIX_ENABLED) @TestApi public EnforcingAdmin( @NonNull String packageName, @NonNull Authority authority, diff --git a/core/java/android/app/admin/LockTaskPolicy.java b/core/java/android/app/admin/LockTaskPolicy.java index 68b4ad84d81a..ab32d46a05ad 100644 --- a/core/java/android/app/admin/LockTaskPolicy.java +++ b/core/java/android/app/admin/LockTaskPolicy.java @@ -19,7 +19,6 @@ package android.app.admin; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; -import android.app.admin.flags.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -135,10 +134,8 @@ public final class LockTaskPolicy extends PolicyValue<LockTaskPolicy> { } private void setPackagesInternal(Set<String> packages) { - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - for (String p : packages) { - PolicySizeVerifier.enforceMaxPackageNameLength(p); - } + for (String p : packages) { + PolicySizeVerifier.enforceMaxPackageNameLength(p); } mPackages = new HashSet<>(packages); } diff --git a/core/java/android/app/admin/PackagePermissionPolicyKey.java b/core/java/android/app/admin/PackagePermissionPolicyKey.java index 1a04f6c908bc..226c576d9bc3 100644 --- a/core/java/android/app/admin/PackagePermissionPolicyKey.java +++ b/core/java/android/app/admin/PackagePermissionPolicyKey.java @@ -25,7 +25,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.admin.flags.Flags; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -59,10 +58,8 @@ public final class PackagePermissionPolicyKey extends PolicyKey { public PackagePermissionPolicyKey(@NonNull String identifier, @NonNull String packageName, @NonNull String permissionName) { super(identifier); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxPackageNameLength(packageName); - PolicySizeVerifier.enforceMaxStringLength(permissionName, "permissionName"); - } + PolicySizeVerifier.enforceMaxPackageNameLength(packageName); + PolicySizeVerifier.enforceMaxStringLength(permissionName, "permissionName"); mPackageName = Objects.requireNonNull((packageName)); mPermissionName = Objects.requireNonNull((permissionName)); } diff --git a/core/java/android/app/admin/PackagePolicyKey.java b/core/java/android/app/admin/PackagePolicyKey.java index 9e31a23aec91..8fa21dbb0a2e 100644 --- a/core/java/android/app/admin/PackagePolicyKey.java +++ b/core/java/android/app/admin/PackagePolicyKey.java @@ -24,7 +24,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.admin.flags.Flags; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -55,9 +54,7 @@ public final class PackagePolicyKey extends PolicyKey { @TestApi public PackagePolicyKey(@NonNull String key, @NonNull String packageName) { super(key); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxPackageNameLength(packageName); - } + PolicySizeVerifier.enforceMaxPackageNameLength(packageName); mPackageName = Objects.requireNonNull((packageName)); } diff --git a/core/java/android/app/admin/PackageSetPolicyValue.java b/core/java/android/app/admin/PackageSetPolicyValue.java index 8b253a23a299..24c50b0994d7 100644 --- a/core/java/android/app/admin/PackageSetPolicyValue.java +++ b/core/java/android/app/admin/PackageSetPolicyValue.java @@ -18,7 +18,6 @@ package android.app.admin; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.admin.flags.Flags; import android.os.Parcel; import java.util.HashSet; @@ -32,10 +31,8 @@ public final class PackageSetPolicyValue extends PolicyValue<Set<String>> { public PackageSetPolicyValue(@NonNull Set<String> value) { super(value); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - for (String packageName : value) { - PolicySizeVerifier.enforceMaxPackageNameLength(packageName); - } + for (String packageName : value) { + PolicySizeVerifier.enforceMaxPackageNameLength(packageName); } } diff --git a/core/java/android/app/admin/StringPolicyValue.java b/core/java/android/app/admin/StringPolicyValue.java index 6efe9ad0dbed..bb07c23163ea 100644 --- a/core/java/android/app/admin/StringPolicyValue.java +++ b/core/java/android/app/admin/StringPolicyValue.java @@ -18,7 +18,6 @@ package android.app.admin; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.admin.flags.Flags; import android.os.Parcel; import java.util.Objects; @@ -30,9 +29,7 @@ public final class StringPolicyValue extends PolicyValue<String> { public StringPolicyValue(@NonNull String value) { super(value); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxStringLength(value, "policyValue"); - } + PolicySizeVerifier.enforceMaxStringLength(value, "policyValue"); } private StringPolicyValue(Parcel source) { diff --git a/core/java/android/app/admin/UserRestrictionPolicyKey.java b/core/java/android/app/admin/UserRestrictionPolicyKey.java index 9054287cb7a0..16cfba4414d5 100644 --- a/core/java/android/app/admin/UserRestrictionPolicyKey.java +++ b/core/java/android/app/admin/UserRestrictionPolicyKey.java @@ -21,7 +21,6 @@ import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_KEY; import android.annotation.NonNull; import android.annotation.SystemApi; import android.annotation.TestApi; -import android.app.admin.flags.Flags; import android.os.Bundle; import android.os.Parcel; @@ -45,9 +44,7 @@ public final class UserRestrictionPolicyKey extends PolicyKey { @TestApi public UserRestrictionPolicyKey(@NonNull String identifier, @NonNull String restriction) { super(identifier); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxStringLength(restriction, "restriction"); - } + PolicySizeVerifier.enforceMaxStringLength(restriction, "restriction"); mRestriction = Objects.requireNonNull(restriction); } diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 29a5048daae6..d9bd77fb3d54 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -4,6 +4,7 @@ package: "android.app.admin.flags" container: "system" +# Fully rolled out and must not be used. flag { name: "policy_engine_migration_v2_enabled" is_exported: true @@ -28,16 +29,6 @@ flag { } flag { - name: "device_policy_size_tracking_internal_bug_fix_enabled" - namespace: "enterprise" - description: "Bug fix for tracking the total policy size and have a max threshold" - bug: "281543351" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "onboarding_bugreport_v2_enabled" is_exported: true namespace: "enterprise" @@ -77,13 +68,6 @@ flag { } flag { - name: "permission_migration_for_zero_trust_impl_enabled" - namespace: "enterprise" - description: "(Implementation) Migrate existing APIs to permission based, and enable DMRH to call them to collect Zero Trust signals." - bug: "289520697" -} - -flag { name: "device_theft_api_enabled" is_exported: true namespace: "enterprise" @@ -105,6 +89,14 @@ flag { bug: "289520697" } +flag { + name: "coexistence_migration_for_supervision_enabled" + is_exported: true + namespace: "enterprise" + description: "Migrate existing APIs that are used by supervision (Kids Module) to be coexistable." + bug: "356894721" +} + # Fully rolled out and must not be used. flag { name: "security_log_v2_enabled" @@ -139,7 +131,6 @@ flag { bug: "293441361" } -# Fully rolled out and must not be used. flag { name: "assist_content_user_restriction_enabled" is_exported: true @@ -166,7 +157,6 @@ flag { bug: "304999634" } -# Fully rolled out and must not be used. flag { name: "esim_management_enabled" is_exported: true @@ -220,16 +210,6 @@ flag { } flag { - name: "headless_device_owner_provisioning_fix_enabled" - namespace: "enterprise" - description: "Fix provisioning for single-user headless DO" - bug: "289515470" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "dmrh_set_app_restrictions" namespace: "enterprise" description: "Allow DMRH to set application restrictions (both on the profile and the parent)" @@ -240,13 +220,6 @@ flag { } flag { - name: "allow_screen_brightness_control_on_cope" - namespace: "enterprise" - description: "Allow COPE admin to control screen brightness and timeout." - bug: "323894620" -} - -flag { name: "always_persist_do" namespace: "enterprise" description: "Always write device_owners2.xml so that migration flags aren't lost" @@ -264,16 +237,6 @@ flag { } flag { - name: "headless_device_owner_delegate_security_logging_bug_fix" - namespace: "enterprise" - description: "Fix delegate security logging for single user headless DO." - bug: "289515470" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "headless_single_user_bad_device_admin_state_fix" namespace: "enterprise" description: "Fix the bad state in DPMS caused by an earlier bug related to the headless single user change" @@ -294,16 +257,6 @@ flag { } flag { - name: "delete_private_space_under_restriction" - namespace: "enterprise" - description: "Delete private space if user restriction is set" - bug: "328758346" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "unmanaged_mode_migration" namespace: "enterprise" description: "Migrate APIs for unmanaged mode" diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 0ee902632f5f..8f609def0b32 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -49,7 +49,7 @@ public final class AppFunctionManager { /** * Creates an instance. * - * @param mService An interface to the backing service. + * @param service An interface to the backing service. * @param context A {@link Context}. * @hide */ diff --git a/core/java/android/app/appfunctions/AppFunctionManagerConfiguration.java b/core/java/android/app/appfunctions/AppFunctionManagerConfiguration.java new file mode 100644 index 000000000000..e4784b4ef69d --- /dev/null +++ b/core/java/android/app/appfunctions/AppFunctionManagerConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 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.app.appfunctions; + +import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.pm.PackageManager; + +/** + * Represents the system configuration of support for the {@code AppFunctionManager} and + * associated systems. + * + * @hide + */ +public class AppFunctionManagerConfiguration { + private final Context mContext; + + /** + * Constructs a new instance of {@code AppFunctionManagerConfiguration}. + * @param context context + */ + public AppFunctionManagerConfiguration(@NonNull final Context context) { + mContext = context; + } + + /** + * Indicates whether the current target is intended to support {@code AppFunctionManager}. + * @return {@code true} if supported; otherwise {@code false} + */ + public boolean isSupported() { + return enableAppFunctionManager() && !isWatch(); + + } + + /** + * Indicates whether the current target is intended to support {@code AppFunctionManager}. + * @param context context + * @return {@code true} if supported; otherwise {@code false} + */ + public static boolean isSupported(@NonNull final Context context) { + return new AppFunctionManagerConfiguration(context).isSupported(); + } + + private boolean isWatch() { + return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); + } +} diff --git a/core/java/android/app/supervision/OWNERS b/core/java/android/app/supervision/OWNERS new file mode 100644 index 000000000000..afc549517abe --- /dev/null +++ b/core/java/android/app/supervision/OWNERS @@ -0,0 +1,2 @@ +jparks@google.com +romkal@google.com diff --git a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl index 7c674f9cde6b..767f52a92566 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl @@ -54,4 +54,13 @@ oneway interface IVirtualDeviceActivityListener { */ void onActivityLaunchBlocked(int displayId, in ComponentName componentName, in UserHandle user, in IntentSender intentSender); + + /** + * Called when a secure surface is shown on the device. + * + * @param displayId The display ID on which the secure surface was shown. + * @param componentName The component name of the activity that showed the secure surface. + * @param user The user associated with the activity. + */ + void onSecureWindowShown(int displayId, in ComponentName componentName, in UserHandle user); } diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java index b7bf2d16ba2c..de20a68e52cb 100644 --- a/core/java/android/companion/virtual/VirtualDeviceInternal.java +++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java @@ -151,7 +151,24 @@ public class VirtualDeviceInternal { Binder.restoreCallingIdentity(token); } } + + @Override + public void onSecureWindowShown(int displayId, ComponentName componentName, + UserHandle user) { + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mActivityListenersLock) { + for (int i = 0; i < mActivityListeners.size(); i++) { + mActivityListeners.valueAt(i) + .onSecureWindowShown(displayId, componentName, user); + } + } + } finally { + Binder.restoreCallingIdentity(token); + } + } }; + private final IVirtualDeviceSoundEffectListener mSoundEffectListener = new IVirtualDeviceSoundEffectListener.Stub() { @Override @@ -584,6 +601,12 @@ public class VirtualDeviceInternal { mActivityListener.onActivityLaunchBlocked( displayId, componentName, user, intentSender)); } + + public void onSecureWindowShown(int displayId, ComponentName componentName, + UserHandle user) { + mExecutor.execute(() -> + mActivityListener.onSecureWindowShown(displayId, componentName, user)); + } } /** diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 40aa6837ad1d..cf3445246fce 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -1255,6 +1255,20 @@ public final class VirtualDeviceManager { @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName, @NonNull UserHandle user, @Nullable IntentSender intentSender) {} + + /** + * Called when a window with a secure surface is shown on the device. + * + * @param displayId The display ID on which the window was shown. + * @param componentName The component name of the activity that showed the window. + * @param user The user associated with the activity. + * + * @see Display#FLAG_SECURE + * @see WindowManager.LayoutParams#FLAG_SECURE + */ + @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API) + default void onSecureWindowShown(int displayId, @NonNull ComponentName componentName, + @NonNull UserHandle user) {} } /** diff --git a/core/java/android/content/rollback/RollbackInfo.java b/core/java/android/content/rollback/RollbackInfo.java index d128055fec6d..a20159da699c 100644 --- a/core/java/android/content/rollback/RollbackInfo.java +++ b/core/java/android/content/rollback/RollbackInfo.java @@ -19,8 +19,6 @@ package android.content.rollback; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; -import android.annotation.TestApi; -import android.content.pm.Flags; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.os.Parcel; @@ -136,11 +134,8 @@ public final class RollbackInfo implements Parcelable { * Get rollback impact level. Refer {@link * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info * on impact level. - * - * @hide */ - @TestApi - @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION) + @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY) public @PackageManager.RollbackImpactLevel int getRollbackImpactLevel() { return mRollbackImpactLevel; } diff --git a/core/java/android/hardware/display/BrightnessInfo.java b/core/java/android/hardware/display/BrightnessInfo.java index 109b0a8be7f8..6a96a54d93ba 100644 --- a/core/java/android/hardware/display/BrightnessInfo.java +++ b/core/java/android/hardware/display/BrightnessInfo.java @@ -158,6 +158,8 @@ public final class BrightnessInfo implements Parcelable { return "thermal"; case BRIGHTNESS_MAX_REASON_POWER_IC: return "power IC"; + case BRIGHTNESS_MAX_REASON_WEAR_BEDTIME_MODE: + return "wear bedtime"; } return "invalid"; } diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index c5d0caf228d2..8592dedbb2bb 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -20,10 +20,12 @@ import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FL import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS; import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG; import static com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG; +import static com.android.hardware.input.Flags.FLAG_KEYBOARD_REPEAT_KEYS; import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag; import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag; import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag; import static com.android.hardware.input.Flags.keyboardA11yMouseKeys; +import static com.android.hardware.input.Flags.keyboardRepeatKeys; import static com.android.hardware.input.Flags.touchpadTapDragging; import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.input.flags.Flags.enableInputFilterRustImpl; @@ -40,6 +42,7 @@ import android.content.Context; import android.os.UserHandle; import android.provider.Settings; import android.sysprop.InputProperties; +import android.view.ViewConfiguration; /** * InputSettings encapsulates reading and writing settings related to input @@ -90,6 +93,30 @@ public class InputSettings { */ public static final int DEFAULT_STYLUS_POINTER_ICON_ENABLED = 1; + /** + * The minimum allowed repeat keys timeout before starting key repeats. + * @hide + */ + public static final int MIN_KEY_REPEAT_TIMEOUT_MILLIS = 150; + + /** + * The maximum allowed repeat keys timeout before starting key repeats. + * @hide + */ + public static final int MAX_KEY_REPEAT_TIMEOUT_MILLIS = 2000; + + /** + * The minimum allowed repeat keys delay between successive key repeats. + * @hide + */ + public static final int MIN_KEY_REPEAT_DELAY_MILLIS = 20; + + /** + * The maximum allowed repeat keys delay between successive key repeats. + * @hide + */ + public static final int MAX_KEY_REPEAT_DELAY_MILLIS = 2000; + private InputSettings() { } @@ -767,4 +794,141 @@ public class InputSettings { Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED, enabled ? 1 : 0, UserHandle.USER_CURRENT); } + + /** + * Whether "Repeat keys" feature flag is enabled. + * + * <p> + * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular + * key on the physical keyboard is held down. This accessibility feature allows the user + * to configure the timeout before the key repeats begin as well as the delay + * between successive key repeats. + * </p> + * + * @hide + */ + public static boolean isRepeatKeysFeatureFlagEnabled() { + return keyboardRepeatKeys(); + } + + /** + * Get Accessibility repeat keys timeout duration in milliseconds. + * The default key repeat timeout is {@link ViewConfiguration#DEFAULT_KEY_REPEAT_TIMEOUT_MS}. + * + * @param context The application context + * @return The time duration for which a key should be pressed after + * which the pressed key will be repeated. The timeout must be between + * {@link #MIN_KEY_REPEAT_TIMEOUT_MILLIS} and + * {@link #MAX_KEY_REPEAT_TIMEOUT_MILLIS} + * + * <p> + * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular + * key on the physical keyboard is held down. This accessibility feature allows the user + * to configure the timeout before the key repeats begin as well as the delay + * between successive key repeats. + * </p> + * + * @hide + */ + @TestApi + @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS) + public static int getAccessibilityRepeatKeysTimeout(@NonNull Context context) { + return Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.KEY_REPEAT_TIMEOUT_MS, ViewConfiguration.getKeyRepeatTimeout(), + UserHandle.USER_CURRENT); + } + + /** + * Get Accessibility repeat keys delay rate in milliseconds. + * The default key repeat delay is {@link ViewConfiguration#DEFAULT_KEY_REPEAT_DELAY_MS}. + * + * @param context The application context + * @return Time duration between successive key repeats when a key is + * pressed down. The delay duration must be between + * {@link #MIN_KEY_REPEAT_DELAY_MILLIS} and + * {@link #MAX_KEY_REPEAT_DELAY_MILLIS} + * + * <p> + * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular + * key on the physical keyboard is held down. This accessibility feature allows the user + * to configure the timeout before the key repeats begin as well as the delay + * between successive key repeats. + * </p> + * + * @hide + */ + @TestApi + @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS) + public static int getAccessibilityRepeatKeysDelay(@NonNull Context context) { + return Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.KEY_REPEAT_DELAY_MS, ViewConfiguration.getKeyRepeatDelay(), + UserHandle.USER_CURRENT); + } + + /** + * Set Accessibility repeat keys timeout duration in milliseconds. + * + * @param timeoutTimeMillis time duration for which a key should be pressed after which the + * pressed key will be repeated. The timeout must be between + * {@link #MIN_KEY_REPEAT_TIMEOUT_MILLIS} and + * {@link #MAX_KEY_REPEAT_TIMEOUT_MILLIS} + * + * <p> + * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular + * key on the physical keyboard is held down. This accessibility feature allows the user + * to configure the timeout before the key repeats begin as well as the delay + * between successive key repeats. + * </p> + * + * @hide + */ + @TestApi + @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS) + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setAccessibilityRepeatKeysTimeout(@NonNull Context context, + int timeoutTimeMillis) { + if (timeoutTimeMillis < MIN_KEY_REPEAT_TIMEOUT_MILLIS + || timeoutTimeMillis > MAX_KEY_REPEAT_TIMEOUT_MILLIS) { + throw new IllegalArgumentException( + "Provided repeat keys timeout should be in range (" + + MIN_KEY_REPEAT_TIMEOUT_MILLIS + "," + + MAX_KEY_REPEAT_TIMEOUT_MILLIS + ")"); + } + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.KEY_REPEAT_TIMEOUT_MS, timeoutTimeMillis, + UserHandle.USER_CURRENT); + } + + /** + * Set Accessibility repeat key delay duration in milliseconds. + * + * @param delayTimeMillis Time duration between successive key repeats when a key is + * pressed down. The delay duration must be between + * {@link #MIN_KEY_REPEAT_DELAY_MILLIS} and + * {@link #MAX_KEY_REPEAT_DELAY_MILLIS} + * <p> + * ‘Repeat keys’ is a feature which allows users to generate key repeats when a particular + * key on the physical keyboard is held down. This accessibility feature allows the user + * to configure the timeout before the key repeats begin as well as the delay + * between successive key repeats. + * </p> + * + * @hide + */ + @TestApi + @FlaggedApi(FLAG_KEYBOARD_REPEAT_KEYS) + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setAccessibilityRepeatKeysDelay(@NonNull Context context, + int delayTimeMillis) { + if (delayTimeMillis < MIN_KEY_REPEAT_DELAY_MILLIS + || delayTimeMillis > MAX_KEY_REPEAT_DELAY_MILLIS) { + throw new IllegalArgumentException( + "Provided repeat keys delay should be in range (" + + MIN_KEY_REPEAT_DELAY_MILLIS + "," + + MAX_KEY_REPEAT_DELAY_MILLIS + ")"); + } + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.KEY_REPEAT_DELAY_MS, delayTimeMillis, + UserHandle.USER_CURRENT); + } } diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 83c4de31824d..077bd821db02 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -99,3 +99,10 @@ flag { description: "Refactor ModifierShortcutManager internal representation of shortcuts." bug: "358603902" } + +flag { + name: "keyboard_repeat_keys" + namespace: "input" + description: "Allow configurable timeout before key repeat and repeat delay rate for key repeats" + bug: "336585002" +} diff --git a/core/java/android/os/TransactionTooLargeException.java b/core/java/android/os/TransactionTooLargeException.java index 79892e060f49..6b7cb3310ee4 100644 --- a/core/java/android/os/TransactionTooLargeException.java +++ b/core/java/android/os/TransactionTooLargeException.java @@ -47,7 +47,7 @@ import android.os.RemoteException; * If possible, try to break up big requests into smaller pieces. * </p><p> * If you are implementing a service, it may help to impose size or complexity - * contraints on the queries that clients can perform. For example, if the result set + * constraints on the queries that clients can perform. For example, if the result set * could become large, then don't allow the client to request more than a few records * at a time. Alternately, instead of returning all of the available data all at once, * return the essential information first and make the client ask for additional information diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 24f52d0151bb..98904fe246f8 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1974,7 +1974,7 @@ public final class Settings { /** * Activity Action: Show Notification Policy access settings. * <p> - * Users can grant and deny access to Notification Policy (DND / Priority Modes) configuration + * Users can grant and deny access to Notification Policy (DND / Modes) configuration * from here. Managed profiles cannot grant Notification Policy access. * See {@link android.app.NotificationManager#isNotificationPolicyAccessGranted()} for more * details. diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 918e591069fb..ba647864a4f1 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1044,23 +1044,19 @@ public class ZenModeConfig implements Parcelable { rt.suppressedVisualEffects = safeInt(parser, DISALLOW_ATT_VISUAL_EFFECTS, DEFAULT_SUPPRESSED_VISUAL_EFFECTS); } else if (MANUAL_TAG.equals(tag)) { - ZenRule manualRule = readRuleXml(parser); - if (manualRule != null) { - rt.manualRule = manualRule; - - // Manual rule may be present prior to modes_ui if it were on, but in that - // case it would not have a set policy, so make note of the need to set - // it up later. - readManualRule = true; - if (rt.manualRule.zenPolicy == null) { - readManualRuleWithoutPolicy = true; - } + rt.manualRule = readRuleXml(parser); + // Manual rule may be present prior to modes_ui if it were on, but in that + // case it would not have a set policy, so make note of the need to set + // it up later. + readManualRule = true; + if (rt.manualRule.zenPolicy == null) { + readManualRuleWithoutPolicy = true; } } else if (AUTOMATIC_TAG.equals(tag) || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) { final String id = parser.getAttributeValue(null, RULE_ATT_ID); - final ZenRule automaticRule = readRuleXml(parser); - if (id != null && automaticRule != null) { + if (id != null) { + final ZenRule automaticRule = readRuleXml(parser); automaticRule.id = id; if (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag)) { String deletedRuleKey = deletedRuleKey(automaticRule); @@ -1177,16 +1173,13 @@ public class ZenModeConfig implements Parcelable { out.endTag(null, ZEN_TAG); } + @NonNull public static ZenRule readRuleXml(TypedXmlPullParser parser) { final ZenRule rt = new ZenRule(); rt.enabled = safeBoolean(parser, RULE_ATT_ENABLED, true); rt.name = parser.getAttributeValue(null, RULE_ATT_NAME); final String zen = parser.getAttributeValue(null, RULE_ATT_ZEN); - rt.zenMode = tryParseZenMode(zen, -1); - if (rt.zenMode == -1) { - Slog.w(TAG, "Bad zen mode in rule xml:" + zen); - return null; - } + rt.zenMode = tryParseZenMode(zen, ZEN_MODE_IMPORTANT_INTERRUPTIONS); rt.conditionId = safeUri(parser, RULE_ATT_CONDITION_ID); rt.component = safeComponentName(parser, RULE_ATT_COMPONENT); rt.configurationActivity = safeComponentName(parser, RULE_ATT_CONFIG_ACTIVITY); diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java index c7e93c19484f..b80146505a1b 100644 --- a/core/java/android/view/ImeBackAnimationController.java +++ b/core/java/android/view/ImeBackAnimationController.java @@ -149,15 +149,17 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { private void setPreCommitProgress(float progress) { if (isHideAnimationInProgress()) return; + setInterpolatedProgress(BACK_GESTURE.getInterpolation(progress) * PEEK_FRACTION); + } + + private void setInterpolatedProgress(float progress) { if (mWindowInsetsAnimationController != null) { float hiddenY = mWindowInsetsAnimationController.getHiddenStateInsets().bottom; float shownY = mWindowInsetsAnimationController.getShownStateInsets().bottom; float imeHeight = shownY - hiddenY; - float interpolatedProgress = BACK_GESTURE.getInterpolation(progress); - int newY = (int) (imeHeight - interpolatedProgress * (imeHeight * PEEK_FRACTION)); + int newY = (int) (imeHeight - progress * imeHeight); if (mStartRootScrollY != 0) { - mViewRoot.setScrollY( - (int) (mStartRootScrollY * (1 - interpolatedProgress * PEEK_FRACTION))); + mViewRoot.setScrollY((int) (mStartRootScrollY * (1 - progress))); } mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, newY), 1f, progress); @@ -171,21 +173,14 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { return; } mTriggerBack = triggerBack; - int currentBottomInset = mWindowInsetsAnimationController.getCurrentInsets().bottom; - int targetBottomInset; - if (triggerBack) { - targetBottomInset = mWindowInsetsAnimationController.getHiddenStateInsets().bottom; - } else { - targetBottomInset = mWindowInsetsAnimationController.getShownStateInsets().bottom; - } - mPostCommitAnimator = ValueAnimator.ofFloat(currentBottomInset, targetBottomInset); + float targetProgress = triggerBack ? 1f : 0f; + mPostCommitAnimator = ValueAnimator.ofFloat( + BACK_GESTURE.getInterpolation(mLastProgress) * PEEK_FRACTION, targetProgress); mPostCommitAnimator.setInterpolator( triggerBack ? STANDARD_ACCELERATE : EMPHASIZED_DECELERATE); mPostCommitAnimator.addUpdateListener(animation -> { - int bottomInset = (int) ((float) animation.getAnimatedValue()); if (mWindowInsetsAnimationController != null) { - mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, bottomInset), - 1f, animation.getAnimatedFraction()); + setInterpolatedProgress((float) animation.getAnimatedValue()); } else { reset(); } @@ -213,14 +208,8 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { notifyHideIme(); // requesting IME as invisible during post-commit mInsetsController.setRequestedVisibleTypes(0, ime()); - // Changes the animation state. This also notifies RootView of changed insets, which - // causes it to reset its scrollY to 0f (animated) if it was panned mInsetsController.onAnimationStateChanged(ime(), /*running*/ true); } - if (mStartRootScrollY != 0 && !triggerBack) { - // This causes RootView to update its scroll back to the panned position - mInsetsController.getHost().notifyInsetsChanged(); - } } private void notifyHideIme() { @@ -282,6 +271,10 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { return mPostCommitAnimator != null && mTriggerBack; } + boolean isAnimationInProgress() { + return mIsPreCommitAnimationInProgress || mWindowInsetsAnimationController != null; + } + /** * Dump information about this ImeBackAnimationController * diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index 6343313b2e01..e90b1c0fc167 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -70,7 +70,14 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { "ImeInsetsSourceConsumer#onAnimationFinished", mController.getHost().getInputMethodManager(), null /* icProto */); } - boolean insetsChanged = super.onAnimationStateChanged(running); + boolean insetsChanged = false; + if (Flags.predictiveBackIme() && !running && isShowRequested() + && mAnimationState == ANIMATION_STATE_HIDE) { + // A user controlled hide animation may have ended in the shown state (e.g. + // cancelled predictive back animation) -> Insets need to be reset to shown. + insetsChanged |= applyLocalVisibilityOverride(); + } + insetsChanged |= super.onAnimationStateChanged(running); if (running && !isShowRequested() && mController.isPredictiveBackImeHideAnimInProgress()) { // IME predictive back animation switched from pre-commit to post-commit. diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 7c8cd932f737..8fdf91a2d87c 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1197,7 +1197,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation pendingRequest.listener, null /* frame */, true /* fromIme */, pendingRequest.mInsetsAnimationSpec, pendingRequest.animationType, pendingRequest.layoutInsetsDuringAnimation, - pendingRequest.useInsetsAnimationThread, statsToken); + pendingRequest.useInsetsAnimationThread, statsToken, + false /* fromPredictiveBack */); } @Override @@ -1307,7 +1308,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation WindowInsetsAnimationControlListener listener, boolean fromIme, long durationMs, @Nullable Interpolator interpolator, @AnimationType int animationType, boolean fromPredictiveBack) { - if ((mState.calculateUncontrollableInsetsFromFrame(mFrame) & types) != 0) { + if ((mState.calculateUncontrollableInsetsFromFrame(mFrame) & types) != 0 + || (fromPredictiveBack && ((mRequestedVisibleTypes & ime()) == 0))) { + // abort if insets are uncontrollable or if control request is from predictive back but + // there is already a hide anim in progress listener.onCancelled(null); return; } @@ -1330,7 +1334,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // TODO(b/342111149): Create statsToken here once ImeTracker#onStart becomes async. controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, spec, animationType, getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack), - false /* useInsetsAnimationThread */, null); + false /* useInsetsAnimationThread */, null, fromPredictiveBack); } private void controlAnimationUnchecked(@InsetsType int types, @@ -1338,7 +1342,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme, InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, - boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) { + boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken, + boolean fromPredictiveBack) { final boolean visible = layoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN; // Basically, we accept the requested visibilities from the upstream callers... @@ -1348,7 +1353,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation // rejecting showing IME. controlAnimationUncheckedInner(types, cancellationSignal, listener, frame, fromIme, insetsAnimationSpec, animationType, layoutInsetsDuringAnimation, - useInsetsAnimationThread, statsToken); + useInsetsAnimationThread, statsToken, fromPredictiveBack); // We are finishing setting the requested visible types. Report them to the server // and/or the app. @@ -1360,7 +1365,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation WindowInsetsAnimationControlListener listener, @Nullable Rect frame, boolean fromIme, InsetsAnimationSpec insetsAnimationSpec, @AnimationType int animationType, @LayoutInsetsDuringAnimation int layoutInsetsDuringAnimation, - boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken) { + boolean useInsetsAnimationThread, @Nullable ImeTracker.Token statsToken, + boolean fromPredictiveBack) { if ((types & mTypesBeingCancelled) != 0) { final boolean monitoredAnimation = animationType == ANIMATION_TYPE_SHOW || animationType == ANIMATION_TYPE_HIDE; @@ -1446,7 +1452,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } else { Pair<Integer, Boolean> typesReadyPair = collectSourceControls( - fromIme, types, controls, animationType, statsToken); + fromIme, types, controls, animationType, statsToken, fromPredictiveBack); typesReady = typesReadyPair.first; boolean imeReady = typesReadyPair.second; if (DEBUG) { @@ -1582,7 +1588,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation */ private Pair<Integer, Boolean> collectSourceControls(boolean fromIme, @InsetsType int types, SparseArray<InsetsSourceControl> controls, @AnimationType int animationType, - @Nullable ImeTracker.Token statsToken) { + @Nullable ImeTracker.Token statsToken, boolean fromPredictiveBack) { ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS); @@ -1594,7 +1600,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation continue; } boolean show = animationType == ANIMATION_TYPE_SHOW - || animationType == ANIMATION_TYPE_USER; + || (animationType == ANIMATION_TYPE_USER + && (!fromPredictiveBack || !mHost.hasAnimationCallbacks())); boolean canRun = true; if (show) { // Show request @@ -1617,7 +1624,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation break; } } else { - consumer.requestHide(fromIme, statsToken); + consumer.requestHide(fromIme + || (fromPredictiveBack && mHost.hasAnimationCallbacks()), statsToken); } if (!canRun) { if (WARN) Log.w(TAG, String.format( @@ -1672,9 +1680,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode( @InsetsType int types, boolean fromPredictiveBack) { - if (fromPredictiveBack) { - // When insets are animated by predictive back, we want insets to be shown to prevent a - // jump cut from shown to hidden at the start of the predictive back animation + if (fromPredictiveBack && !mHost.hasAnimationCallbacks()) { + // When insets are animated by predictive back and the app does not have an animation + // callback, we want insets to be shown to prevent a jump cut from shown to hidden at + // the start of the predictive back animation return LAYOUT_INSETS_DURING_ANIMATION_SHOWN; } // Generally, we want to layout the opposite of the current state. This is to make animation @@ -2021,7 +2030,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation listener /* insetsAnimationSpec */, show ? ANIMATION_TYPE_SHOW : ANIMATION_TYPE_HIDE, show ? LAYOUT_INSETS_DURING_ANIMATION_SHOWN : LAYOUT_INSETS_DURING_ANIMATION_HIDDEN, - !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken); + !hasAnimationCallbacks /* useInsetsAnimationThread */, statsToken, + false /* fromPredictiveBack */); } /** diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 2dda835436bc..9e4b27d3fa55 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -445,20 +445,16 @@ public final class SurfaceControl implements Parcelable { // Jank due to unknown reasons. public static final int UNKNOWN = 0x80; - public JankData(long frameVsyncId, @JankType int jankType, long frameIntervalNs, - long scheduledAppFrameTimeNs, long actualAppFrameTimeNs) { + public JankData(long frameVsyncId, @JankType int jankType, long frameIntervalNs) { this.frameVsyncId = frameVsyncId; this.jankType = jankType; this.frameIntervalNs = frameIntervalNs; - this.scheduledAppFrameTimeNs = scheduledAppFrameTimeNs; - this.actualAppFrameTimeNs = actualAppFrameTimeNs; + } public final long frameVsyncId; public final @JankType int jankType; public final long frameIntervalNs; - public final long scheduledAppFrameTimeNs; - public final long actualAppFrameTimeNs; } /** diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index e81f32e1e64b..523ff38550c1 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -141,7 +141,6 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; -import android.os.Vibrator; import android.service.credentials.CredentialProviderService; import android.sysprop.DisplayProperties; import android.text.InputType; @@ -34156,7 +34155,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * REQUESTED_FRAME_RATE_CATEGORY_NORMAL, REQUESTED_FRAME_RATE_CATEGORY_HIGH. * Keep in mind that the preferred frame rate affects the frame rate for the next frame, * so use this method carefully. It's important to note that the preference is valid as - * long as the View is invalidated. + * long as the View is invalidated. Please also be aware that the requested frame rate + * will not propagate to child views when this API is used on a ViewGroup. * * @param frameRate the preferred frame rate of the view. */ @@ -34175,6 +34175,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE, REQUESTED_FRAME_RATE_CATEGORY_LOW, * REQUESTED_FRAME_RATE_CATEGORY_NORMAL, and REQUESTED_FRAME_RATE_CATEGORY_HIGH. * Note that the frame rate value is valid as long as the View is invalidated. + * Please also be aware that the requested frame rate will not propagate to + * child views when this API is used on a ViewGroup. * * @return REQUESTED_FRAME_RATE_CATEGORY_DEFAULT by default, * or value passed to {@link #setRequestedFrameRate(float)}. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 0e1625aaedd8..f021bdfe478f 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6094,6 +6094,12 @@ public final class ViewRootImpl implements ViewParent, } boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) { + if (mImeBackAnimationController.isAnimationInProgress()) { + // IME predictive back animation is currently in progress which means that scrollY is + // currently controlled by ImeBackAnimationController. + return false; + } + final Rect ci = mAttachInfo.mContentInsets; final Rect vi = mAttachInfo.mVisibleInsets; int scrollY = 0; diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index ab29df357268..a87e5c8e1b56 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -2095,9 +2095,7 @@ public final class AccessibilityManager { * {@link android.view.Display#DEFAULT_DISPLAY}, is or lower than * {@link android.view.Display#INVALID_DISPLAY}, or is already being proxy-ed. * - * @throws SecurityException if the app does not hold the - * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission or the - * {@link Manifest.permission#CREATE_VIRTUAL_DEVICE} permission. + * @throws SecurityException if the app does not hold the required permissions. * * @hide */ @@ -2125,9 +2123,7 @@ public final class AccessibilityManager { * * @return {@code true} if the proxy is successfully unregistered. * - * @throws SecurityException if the app does not hold the - * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission or the - * {@link Manifest.permission#CREATE_VIRTUAL_DEVICE} permission. + * @throws SecurityException if the app does not hold the required permissions. * * @hide */ @@ -2180,8 +2176,8 @@ public final class AccessibilityManager { try { return service.startFlashNotificationSequence(context.getOpPackageName(), reason, mBinder); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while start flash notification sequence", re); + } catch (RemoteException | SecurityException e) { + Log.e(LOG_TAG, "Error while start flash notification sequence", e); return false; } } @@ -2210,8 +2206,8 @@ public final class AccessibilityManager { try { return service.stopFlashNotificationSequence(context.getOpPackageName()); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while stop flash notification sequence", re); + } catch (RemoteException | SecurityException e) { + Log.e(LOG_TAG, "Error while stop flash notification sequence", e); return false; } } @@ -2238,8 +2234,8 @@ public final class AccessibilityManager { try { return service.startFlashNotificationEvent(context.getOpPackageName(), reason, reasonPkg); - } catch (RemoteException re) { - Log.e(LOG_TAG, "Error while start flash notification event", re); + } catch (RemoteException | SecurityException e) { + Log.e(LOG_TAG, "Error while start flash notification event", e); return false; } } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index bf79a2c6c6ea..2de3ce8532e3 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -157,13 +157,13 @@ interface IAccessibilityManager { @EnforcePermission("INJECT_EVENTS") void injectInputEventToInputFilter(in InputEvent event); - @RequiresNoPermission + @EnforcePermission("MANAGE_ACCESSIBILITY") boolean startFlashNotificationSequence(String opPkg, int reason, IBinder token); - @RequiresNoPermission + @EnforcePermission("MANAGE_ACCESSIBILITY") boolean stopFlashNotificationSequence(String opPkg); - @RequiresNoPermission + @EnforcePermission("MANAGE_ACCESSIBILITY") boolean startFlashNotificationEvent(String opPkg, int reason, String reasonPkg); @RequiresNoPermission diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index c53a0e158dea..f53293467a8d 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -318,7 +318,7 @@ public final class WebViewFactory { String libraryFileName; try { PackageInfo packageInfo = packageManager.getPackageInfo(packageName, - PackageManager.GET_META_DATA | PackageManager.MATCH_DEBUG_TRIAGED_MISSING); + PackageManager.GET_META_DATA); libraryFileName = getWebViewLibrary(packageInfo.applicationInfo); } catch (PackageManager.NameNotFoundException e) { Log.e(LOGTAG, "Couldn't find package " + packageName); @@ -479,7 +479,6 @@ public final class WebViewFactory { newPackageInfo = pm.getPackageInfo( response.packageInfo.packageName, PackageManager.GET_SHARED_LIBRARY_FILES - | PackageManager.MATCH_DEBUG_TRIAGED_MISSING // Make sure that we fetch the current provider even if its not // installed for the current user | PackageManager.MATCH_UNINSTALLED_PACKAGES diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig index defe61e506af..b21a490cc506 100644 --- a/core/java/android/webkit/flags.aconfig +++ b/core/java/android/webkit/flags.aconfig @@ -9,3 +9,12 @@ flag { bug: "319292658" is_fixed_read_only: true } + +flag { + name: "mainline_apis" + is_exported: true + namespace: "webview" + description: "New APIs required by WebViewBootstrap mainline module" + bug: "310653407" + is_fixed_read_only: true +} diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig index 6ce9725f95b0..cd31850b281c 100644 --- a/core/java/android/window/flags/responsible_apis.aconfig +++ b/core/java/android/window/flags/responsible_apis.aconfig @@ -71,3 +71,11 @@ flag { bug: "339720406" } +flag { + name: "bal_reduce_grace_period" + namespace: "responsible_apis" + description: "Changes to reduce or ideally remove the grace period exemption." + bug: "362575865" +} + + diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java index 9f5ed65fa252..21fbf9d03c71 100644 --- a/core/java/com/android/internal/display/BrightnessSynchronizer.java +++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java @@ -134,7 +134,8 @@ public class BrightnessSynchronizer { * Prints data on dumpsys. */ public void dump(PrintWriter pw) { - pw.println("BrightnessSynchronizer"); + pw.println("BrightnessSynchronizer:"); + pw.println("-----------------------"); pw.println(" mLatestIntBrightness=" + mLatestIntBrightness); pw.println(" mLatestFloatBrightness=" + mLatestFloatBrightness); pw.println(" mCurrentUpdate=" + mCurrentUpdate); diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index d474c6db4f02..53ef49bd3f65 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -127,7 +127,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai private Runnable mWaitForFinishTimedOut; private static class JankInfo { - final long frameVsyncId; + long frameVsyncId; long totalDurationNanos; boolean isFirstFrame; boolean hwuiCallbackFired; @@ -135,42 +135,29 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai @JankType int jankType; @RefreshRate int refreshRate; - static JankInfo createFromHwuiCallback( - long frameVsyncId, long totalDurationNanos, boolean isFirstFrame) { - return new JankInfo(frameVsyncId).update(totalDurationNanos, isFirstFrame); + static JankInfo createFromHwuiCallback(long frameVsyncId, long totalDurationNanos, + boolean isFirstFrame) { + return new JankInfo(frameVsyncId, true, false, JANK_NONE, UNKNOWN_REFRESH_RATE, + totalDurationNanos, isFirstFrame); } - static JankInfo createFromSurfaceControlCallback(SurfaceControl.JankData jankStat) { - return new JankInfo(jankStat.frameVsyncId).update(jankStat); + static JankInfo createFromSurfaceControlCallback(long frameVsyncId, + @JankType int jankType, @RefreshRate int refreshRate) { + return new JankInfo( + frameVsyncId, false, true, jankType, refreshRate, 0, false /* isFirstFrame */); } - private JankInfo(long frameVsyncId) { + private JankInfo(long frameVsyncId, boolean hwuiCallbackFired, + boolean surfaceControlCallbackFired, @JankType int jankType, + @RefreshRate int refreshRate, + long totalDurationNanos, boolean isFirstFrame) { this.frameVsyncId = frameVsyncId; - this.hwuiCallbackFired = false; - this.surfaceControlCallbackFired = false; - this.jankType = JANK_NONE; - this.refreshRate = UNKNOWN_REFRESH_RATE; - this.totalDurationNanos = 0; - this.isFirstFrame = false; - } - - private JankInfo update(SurfaceControl.JankData jankStat) { - this.surfaceControlCallbackFired = true; - this.jankType = jankStat.jankType; - this.refreshRate = DisplayRefreshRate.getRefreshRate(jankStat.frameIntervalNs); - if (Flags.useSfFrameDuration()) { - this.totalDurationNanos = jankStat.actualAppFrameTimeNs; - } - return this; - } - - private JankInfo update(long totalDurationNanos, boolean isFirstFrame) { - this.hwuiCallbackFired = true; - if (!Flags.useSfFrameDuration()) { - this.totalDurationNanos = totalDurationNanos; - } + this.hwuiCallbackFired = hwuiCallbackFired; + this.surfaceControlCallbackFired = surfaceControlCallbackFired; + this.jankType = jankType; + this.refreshRate = refreshRate; + this.totalDurationNanos = totalDurationNanos; this.isFirstFrame = isFirstFrame; - return this; } @Override @@ -470,12 +457,16 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai if (!isInRange(jankStat.frameVsyncId)) { continue; } + int refreshRate = DisplayRefreshRate.getRefreshRate(jankStat.frameIntervalNs); JankInfo info = findJankInfo(jankStat.frameVsyncId); if (info != null) { - info.update(jankStat); + info.surfaceControlCallbackFired = true; + info.jankType = jankStat.jankType; + info.refreshRate = refreshRate; } else { mJankInfos.put((int) jankStat.frameVsyncId, - JankInfo.createFromSurfaceControlCallback(jankStat)); + JankInfo.createFromSurfaceControlCallback( + jankStat.frameVsyncId, jankStat.jankType, refreshRate)); } } processJankInfos(); @@ -522,7 +513,9 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai } JankInfo info = findJankInfo(frameVsyncId); if (info != null) { - info.update(totalDurationNanos, isFirstFrame); + info.hwuiCallbackFired = true; + info.totalDurationNanos = totalDurationNanos; + info.isFirstFrame = isFirstFrame; } else { mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback( frameVsyncId, totalDurationNanos, isFirstFrame)); diff --git a/core/java/com/android/internal/jank/flags.aconfig b/core/java/com/android/internal/jank/flags.aconfig deleted file mode 100644 index 676bb70e7797..000000000000 --- a/core/java/com/android/internal/jank/flags.aconfig +++ /dev/null @@ -1,9 +0,0 @@ -package: "com.android.internal.jank" -container: "system" - -flag { - name: "use_sf_frame_duration" - namespace: "android_platform_window_surfaces" - description: "Whether to get the frame duration from SurfaceFlinger, or HWUI" - bug: "354763298" -} diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index b9cc457fecc9..2acda8ad71c1 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -631,21 +631,20 @@ public class ZygoteInit { */ private static Runnable forkSystemServer(String abiList, String socketName, ZygoteServer zygoteServer) { - long capabilities = posixCapabilitiesAsBits( - OsConstants.CAP_IPC_LOCK, - OsConstants.CAP_KILL, - OsConstants.CAP_NET_ADMIN, - OsConstants.CAP_NET_BIND_SERVICE, - OsConstants.CAP_NET_BROADCAST, - OsConstants.CAP_NET_RAW, - OsConstants.CAP_SYS_MODULE, - OsConstants.CAP_SYS_NICE, - OsConstants.CAP_SYS_PTRACE, - OsConstants.CAP_SYS_TIME, - OsConstants.CAP_SYS_TTY_CONFIG, - OsConstants.CAP_WAKE_ALARM, - OsConstants.CAP_BLOCK_SUSPEND - ); + long capabilities = + (1L << OsConstants.CAP_IPC_LOCK) | + (1L << OsConstants.CAP_KILL) | + (1L << OsConstants.CAP_NET_ADMIN) | + (1L << OsConstants.CAP_NET_BIND_SERVICE) | + (1L << OsConstants.CAP_NET_BROADCAST) | + (1L << OsConstants.CAP_NET_RAW) | + (1L << OsConstants.CAP_SYS_MODULE) | + (1L << OsConstants.CAP_SYS_NICE) | + (1L << OsConstants.CAP_SYS_PTRACE) | + (1L << OsConstants.CAP_SYS_TIME) | + (1L << OsConstants.CAP_SYS_TTY_CONFIG) | + (1L << OsConstants.CAP_WAKE_ALARM) | + (1L << OsConstants.CAP_BLOCK_SUSPEND); /* Containers run without some capabilities, so drop any caps that are not available. */ StructCapUserHeader header = new StructCapUserHeader( OsConstants._LINUX_CAPABILITY_VERSION_3, 0); @@ -742,20 +741,6 @@ public class ZygoteInit { } /** - * Gets the bit array representation of the provided list of POSIX capabilities. - */ - private static long posixCapabilitiesAsBits(int... capabilities) { - long result = 0; - for (int capability : capabilities) { - if ((capability < 0) || (capability > OsConstants.CAP_LAST_CAP)) { - throw new IllegalArgumentException(String.valueOf(capability)); - } - result |= (1L << capability); - } - return result; - } - - /** * This is the entry point for a Zygote process. It creates the Zygote server, loads resources, * and handles other tasks related to preparing the process for forking into applications. * diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 2ff8c8c4db37..42643588da45 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -930,37 +930,47 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto } private static class Message { + @Nullable private final Long mMessageHash; + @Nullable private final Integer mMessageMask; + @Nullable private final String mMessageString; - private Message(Long messageHash, int messageMask) { + private Message(long messageHash, int messageMask) { this.mMessageHash = messageHash; this.mMessageMask = messageMask; this.mMessageString = null; } - private Message(String messageString) { + private Message(@NonNull String messageString) { this.mMessageHash = null; final List<Integer> argTypes = LogDataType.parseFormatString(messageString); this.mMessageMask = LogDataType.logDataTypesToBitMask(argTypes); this.mMessageString = messageString; } - private int getMessageMask() { + @Nullable + private Integer getMessageMask() { return mMessageMask; } + @Nullable private String getMessage() { return mMessageString; } + @Nullable private String getMessage(@NonNull ProtoLogViewerConfigReader viewerConfigReader) { if (mMessageString != null) { return mMessageString; } - return viewerConfigReader.getViewerString(mMessageHash); + if (mMessageHash != null) { + return viewerConfigReader.getViewerString(mMessageHash); + } + + throw new RuntimeException("Both mMessageString and mMessageHash should never be null"); } } } diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java index 176573870679..eeac1392e4d1 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java @@ -23,6 +23,7 @@ import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Gro import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID; import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.PROTOLOG_VIEWER_CONFIG; @@ -210,8 +211,7 @@ public final class ProtoLogConfigurationService extends IProtoLogConfigurationSe * want to write to the trace buffer. * @throws FileNotFoundException if the viewerConfigFilePath is invalid. */ - void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath) - throws FileNotFoundException; + void trace(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath); } @Override @@ -351,11 +351,7 @@ public final class ProtoLogConfigurationService extends IProtoLogConfigurationSe private void onTracingInstanceFlush() { for (String fileName : mConfigFileCounts.keySet()) { - try { - mViewerConfigFileTracer.trace(mDataSource, fileName); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } + mViewerConfigFileTracer.trace(mDataSource, fileName); } } @@ -364,10 +360,16 @@ public final class ProtoLogConfigurationService extends IProtoLogConfigurationSe } private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource, - @NonNull String viewerConfigFilePath) throws FileNotFoundException { - final var pis = new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); - + @NonNull String viewerConfigFilePath) { dataSource.trace(ctx -> { + final ProtoInputStream pis; + try { + pis = new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); + } catch (FileNotFoundException e) { + throw new RuntimeException( + "Failed to load viewer config file " + viewerConfigFilePath, e); + } + try { final ProtoOutputStream os = ctx.newTracePacket(); @@ -396,11 +398,7 @@ public final class ProtoLogConfigurationService extends IProtoLogConfigurationSe mConfigFileCounts.put(configFile, newCount); boolean lastProcessWithViewerConfig = newCount == 0; if (lastProcessWithViewerConfig) { - try { - mViewerConfigFileTracer.trace(mDataSource, configFile); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } + mViewerConfigFileTracer.trace(mDataSource, configFile); } } } @@ -446,6 +444,7 @@ public final class ProtoLogConfigurationService extends IProtoLogConfigurationSe case (int) MESSAGE -> os.write(MESSAGE, pis.readString(MESSAGE)); case (int) LEVEL -> os.write(LEVEL, pis.readInt(LEVEL)); case (int) GROUP_ID -> os.write(GROUP_ID, pis.readInt(GROUP_ID)); + case (int) LOCATION -> os.write(LOCATION, pis.readString(LOCATION)); default -> throw new RuntimeException( "Unexpected field id " + pis.getFieldNumber()); diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index ef6bece0cc0c..837622f776de 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -37,6 +37,7 @@ import android.tracing.perfetto.StopCallbackArguments; import android.util.proto.ProtoInputStream; import android.util.proto.WireTypeMismatchException; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.LogLevel; import java.io.IOException; @@ -48,6 +49,7 @@ import java.util.Set; public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, ProtoLogDataSource.TlsState, ProtoLogDataSource.IncrementalState> { + private static final String DATASOURCE_NAME = "android.protolog"; private final Instance.TracingInstanceStartCallback mOnStart; private final Runnable mOnFlush; @@ -55,7 +57,13 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, public ProtoLogDataSource(Instance.TracingInstanceStartCallback onStart, Runnable onFlush, Instance.TracingInstanceStopCallback onStop) { - super("android.protolog"); + this(onStart, onFlush, onStop, DATASOURCE_NAME); + } + + @VisibleForTesting + public ProtoLogDataSource(Instance.TracingInstanceStartCallback onStart, Runnable onFlush, + Instance.TracingInstanceStopCallback onStop, String dataSourceName) { + super(dataSourceName); this.mOnStart = onStart; this.mOnFlush = onFlush; this.mOnStop = onStop; diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index 38ca0d8f75e8..3b24f278438d 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -3,7 +3,6 @@ package com.android.internal.protolog; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME; - import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID; @@ -11,7 +10,6 @@ import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Mes import android.annotation.NonNull; import android.annotation.Nullable; -import android.util.Log; import android.util.LongSparseArray; import android.util.proto.ProtoInputStream; @@ -38,6 +36,7 @@ public class ProtoLogViewerConfigReader { * Returns message format string for its hash or null if unavailable * or the viewer config is not loaded into memory. */ + @Nullable public synchronized String getViewerString(long messageHash) { return mLogMessageMap.get(messageHash); } diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 2abdd57662eb..90cb10aa62b2 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -108,6 +108,7 @@ cc_library_shared_for_libandroid_runtime { "libtracing_perfetto", "libharfbuzz_ng", "liblog", + "libmediautils", "libminikin", "libz", "server_configurable_flags", diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 638591f130ab..46710b5d3edc 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -17,6 +17,7 @@ //#define LOG_NDEBUG 0 +#include <atomic> #define LOG_TAG "AudioSystem-JNI" #include <android/binder_ibinder_jni.h> #include <android/binder_libbinder.h> @@ -34,15 +35,16 @@ #include <media/AudioContainers.h> #include <media/AudioPolicy.h> #include <media/AudioSystem.h> +#include <mediautils/jthread.h> #include <nativehelper/JNIHelp.h> #include <nativehelper/ScopedLocalRef.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <nativehelper/jni_macros.h> #include <system/audio.h> #include <system/audio_policy.h> +#include <sys/system_properties.h> #include <utils/Log.h> -#include <thread> #include <optional> #include <sstream> #include <memory> @@ -57,6 +59,7 @@ #include "android_media_AudioMixerAttributes.h" #include "android_media_AudioProfile.h" #include "android_media_MicrophoneInfo.h" +#include "android_media_JNIUtils.h" #include "android_util_Binder.h" #include "core_jni_helpers.h" @@ -3375,42 +3378,53 @@ static jboolean android_media_AudioSystem_isBluetoothVariableLatencyEnabled(JNIE class JavaSystemPropertyListener { public: JavaSystemPropertyListener(JNIEnv* env, jobject javaCallback, std::string sysPropName) : - mCallback(env->NewGlobalRef(javaCallback)), - mCachedProperty(android::base::CachedProperty{std::move(sysPropName)}) { - mListenerThread = std::thread([this]() mutable { - JNIEnv* threadEnv = GetOrAttachJNIEnvironment(gVm); - while (!mCleanupSignal.load()) { - using namespace std::chrono_literals; - // 1s timeout so this thread can read the cleanup signal to (slowly) be able to - // be destroyed. - std::string newVal = mCachedProperty.WaitForChange(1000ms) ?: ""; - if (newVal != "" && mLastVal != newVal) { - threadEnv->CallVoidMethod(mCallback, gRunnableClassInfo.run); - mLastVal = std::move(newVal); + mCallback {javaCallback, env}, + mPi {__system_property_find(sysPropName.c_str())}, + mListenerThread([this](mediautils::stop_token stok) mutable { + static const struct timespec close_delay = { .tv_sec = 1 }; + while (!stok.stop_requested()) { + uint32_t old_serial = mSerial.load(); + uint32_t new_serial; + if (__system_property_wait(mPi, old_serial, &new_serial, &close_delay)) { + while (new_serial > old_serial) { + if (mSerial.compare_exchange_weak(old_serial, new_serial)) { + fireUpdate(); + break; + } + } + } } + }) {} + + void triggerUpdateIfChanged() { + uint32_t old_serial = mSerial.load(); + uint32_t new_serial = __system_property_serial(mPi); + while (new_serial > old_serial) { + if (mSerial.compare_exchange_weak(old_serial, new_serial)) { + fireUpdate(); + break; } - }); + } } - ~JavaSystemPropertyListener() { - mCleanupSignal.store(true); - mListenerThread.join(); - JNIEnv* env = GetOrAttachJNIEnvironment(gVm); - env->DeleteGlobalRef(mCallback); + private: + void fireUpdate() { + const auto threadEnv = GetOrAttachJNIEnvironment(gVm); + threadEnv->CallVoidMethod(mCallback.get(), gRunnableClassInfo.run); } - private: - jobject mCallback; - android::base::CachedProperty mCachedProperty; - std::thread mListenerThread; - std::atomic<bool> mCleanupSignal{false}; - std::string mLastVal = ""; + // Should outlive thread object + const GlobalRef mCallback; + const prop_info * const mPi; + std::atomic<uint32_t> mSerial = 0; + const mediautils::jthread mListenerThread; }; +// A logical set keyed by address std::vector<std::unique_ptr<JavaSystemPropertyListener>> gSystemPropertyListeners; std::mutex gSysPropLock{}; -static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env, jobject thiz, +static jlong android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env, jobject thiz, jstring sysProp, jobject javaCallback) { ScopedUtfChars sysPropChars{env, sysProp}; @@ -3418,6 +3432,19 @@ static void android_media_AudioSystem_listenForSystemPropertyChange(JNIEnv *env, std::string{sysPropChars.c_str()}); std::unique_lock _l{gSysPropLock}; gSystemPropertyListeners.push_back(std::move(listener)); + return reinterpret_cast<jlong>(gSystemPropertyListeners.back().get()); +} + +static void android_media_AudioSystem_triggerSystemPropertyUpdate(JNIEnv *env, jobject thiz, + jlong nativeHandle) { + std::unique_lock _l{gSysPropLock}; + const auto iter = std::find_if(gSystemPropertyListeners.begin(), gSystemPropertyListeners.end(), + [nativeHandle](const auto& x) { return reinterpret_cast<jlong>(x.get()) == nativeHandle; }); + if (iter != gSystemPropertyListeners.end()) { + (*iter)->triggerUpdateIfChanged(); + } else { + jniThrowException(env, "java/lang/IllegalArgumentException", "Invalid handle"); + } } @@ -3595,8 +3622,11 @@ static const JNINativeMethod gMethods[] = MAKE_AUDIO_SYSTEM_METHOD(setBluetoothVariableLatencyEnabled), MAKE_AUDIO_SYSTEM_METHOD(isBluetoothVariableLatencyEnabled), MAKE_JNI_NATIVE_METHOD("listenForSystemPropertyChange", - "(Ljava/lang/String;Ljava/lang/Runnable;)V", + "(Ljava/lang/String;Ljava/lang/Runnable;)J", android_media_AudioSystem_listenForSystemPropertyChange), + MAKE_JNI_NATIVE_METHOD("triggerSystemPropertyUpdate", + "(J)V", + android_media_AudioSystem_triggerSystemPropertyUpdate), }; diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 17c89f88b441..0f531641903a 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -2089,11 +2089,9 @@ public: jobjectArray jJankDataArray = env->NewObjectArray(jankData.size(), gJankDataClassInfo.clazz, nullptr); for (size_t i = 0; i < jankData.size(); i++) { - jobject jJankData = - env->NewObject(gJankDataClassInfo.clazz, gJankDataClassInfo.ctor, - jankData[i].frameVsyncId, jankData[i].jankType, - jankData[i].frameIntervalNs, jankData[i].scheduledAppFrameTimeNs, - jankData[i].actualAppFrameTimeNs); + jobject jJankData = env->NewObject(gJankDataClassInfo.clazz, gJankDataClassInfo.ctor, + jankData[i].frameVsyncId, jankData[i].jankType, + jankData[i].frameIntervalNs); env->SetObjectArrayElement(jJankDataArray, i, jJankData); env->DeleteLocalRef(jJankData); } @@ -2729,7 +2727,7 @@ int register_android_view_SurfaceControl(JNIEnv* env) jclass jankDataClazz = FindClassOrDie(env, "android/view/SurfaceControl$JankData"); gJankDataClassInfo.clazz = MakeGlobalRefOrDie(env, jankDataClazz); - gJankDataClassInfo.ctor = GetMethodIDOrDie(env, gJankDataClassInfo.clazz, "<init>", "(JIJJJ)V"); + gJankDataClassInfo.ctor = GetMethodIDOrDie(env, gJankDataClassInfo.clazz, "<init>", "(JIJ)V"); jclass onJankDataListenerClazz = FindClassOrDie(env, "android/view/SurfaceControl$OnJankDataListener"); gJankDataListenerClassInfo.clazz = MakeGlobalRefOrDie(env, onJankDataListenerClazz); diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp index fba0d81d431f..7ad18b83f0d6 100644 --- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp +++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp @@ -17,6 +17,7 @@ #define LOG_TAG "NativeLibraryHelper" //#define LOG_NDEBUG 0 +#include <android-base/properties.h> #include <androidfw/ApkParsing.h> #include <androidfw/ZipFileRO.h> #include <androidfw/ZipUtils.h> @@ -36,6 +37,7 @@ #include <zlib.h> #include <memory> +#include <string> #include "com_android_internal_content_FileSystemUtils.h" #include "core_jni_helpers.h" @@ -125,72 +127,10 @@ sumFiles(JNIEnv*, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntry, const char return INSTALL_SUCCEEDED; } -/* - * Copy the native library if needed. - * - * This function assumes the library and path names passed in are considered safe. - */ -static install_status_t -copyFileIfChanged(JNIEnv *env, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntry, const char* fileName) -{ - static const size_t kPageSize = getpagesize(); - void** args = reinterpret_cast<void**>(arg); - jstring* javaNativeLibPath = (jstring*) args[0]; - jboolean extractNativeLibs = *(jboolean*) args[1]; - jboolean debuggable = *(jboolean*) args[2]; - - ScopedUtfChars nativeLibPath(env, *javaNativeLibPath); - - uint32_t uncompLen; - uint32_t when; - uint32_t crc; - - uint16_t method; - off64_t offset; - uint16_t extraFieldLength; - if (!zipFile->getEntryInfo(zipEntry, &method, &uncompLen, nullptr, &offset, &when, &crc, - &extraFieldLength)) { - ALOGE("Couldn't read zip entry info\n"); - return INSTALL_FAILED_INVALID_APK; - } - - // Always extract wrap.sh for debuggable, even if extractNativeLibs=false. This makes it - // easier to use wrap.sh because it only works when it is extracted, see - // frameworks/base/services/core/java/com/android/server/am/ProcessList.java. - bool forceExtractCurrentFile = debuggable && strcmp(fileName, "wrap.sh") == 0; - - if (!extractNativeLibs && !forceExtractCurrentFile) { - // check if library is uncompressed and page-aligned - if (method != ZipFileRO::kCompressStored) { - ALOGE("Library '%s' is compressed - will not be able to open it directly from apk.\n", - fileName); - return INSTALL_FAILED_INVALID_APK; - } - - if (offset % kPageSize != 0) { - ALOGE("Library '%s' is not PAGE(%zu)-aligned - will not be able to open it directly " - "from apk.\n", fileName, kPageSize); - return INSTALL_FAILED_INVALID_APK; - } - -#ifdef ENABLE_PUNCH_HOLES - // if library is uncompressed, punch hole in it in place - if (!punchHolesInElf64(zipFile->getZipFileName(), offset)) { - ALOGW("Failed to punch uncompressed elf file :%s inside apk : %s at offset: " - "%" PRIu64 "", - fileName, zipFile->getZipFileName(), offset); - } - - // if extra field for this zip file is present with some length, possibility is that it is - // padding added for zip alignment. Punch holes there too. - if (!punchHolesInZip(zipFile->getZipFileName(), offset, extraFieldLength)) { - ALOGW("Failed to punch apk : %s at extra field", zipFile->getZipFileName()); - } -#endif // ENABLE_PUNCH_HOLES - - return INSTALL_SUCCEEDED; - } - +static install_status_t extractNativeLibFromApk(ZipFileRO* zipFile, ZipEntryRO zipEntry, + const char* fileName, + const std::string nativeLibPath, uint32_t when, + uint32_t uncompLen, uint32_t crc) { // Build local file path const size_t fileNameLen = strlen(fileName); char localFileName[nativeLibPath.size() + fileNameLen + 2]; @@ -313,6 +253,88 @@ copyFileIfChanged(JNIEnv *env, void* arg, ZipFileRO* zipFile, ZipEntryRO zipEntr } /* + * Copy the native library if needed. + * + * This function assumes the library and path names passed in are considered safe. + */ +static install_status_t copyFileIfChanged(JNIEnv* env, void* arg, ZipFileRO* zipFile, + ZipEntryRO zipEntry, const char* fileName) { + static const size_t kPageSize = getpagesize(); + void** args = reinterpret_cast<void**>(arg); + jstring* javaNativeLibPath = (jstring*)args[0]; + jboolean extractNativeLibs = *(jboolean*)args[1]; + jboolean debuggable = *(jboolean*)args[2]; + jboolean app_compat_16kb = *(jboolean*)args[3]; + install_status_t ret = INSTALL_SUCCEEDED; + + ScopedUtfChars nativeLibPath(env, *javaNativeLibPath); + + uint32_t uncompLen; + uint32_t when; + uint32_t crc; + + uint16_t method; + off64_t offset; + uint16_t extraFieldLength; + if (!zipFile->getEntryInfo(zipEntry, &method, &uncompLen, nullptr, &offset, &when, &crc, + &extraFieldLength)) { + ALOGE("Couldn't read zip entry info\n"); + return INSTALL_FAILED_INVALID_APK; + } + + // Always extract wrap.sh for debuggable, even if extractNativeLibs=false. This makes it + // easier to use wrap.sh because it only works when it is extracted, see + // frameworks/base/services/core/java/com/android/server/am/ProcessList.java. + bool forceExtractCurrentFile = debuggable && strcmp(fileName, "wrap.sh") == 0; + + if (!extractNativeLibs && !forceExtractCurrentFile) { + // check if library is uncompressed and page-aligned + if (method != ZipFileRO::kCompressStored) { + ALOGE("Library '%s' is compressed - will not be able to open it directly from apk.\n", + fileName); + return INSTALL_FAILED_INVALID_APK; + } + + if (offset % kPageSize != 0) { + // If the library is zip-aligned correctly for 4kb devices and app compat is + // enabled, on 16kb devices fallback to extraction + if (offset % 0x1000 == 0 && app_compat_16kb) { + ALOGI("16kB AppCompat: Library '%s' is not PAGE(%zu)-aligned - falling back to " + "extraction from apk\n", + fileName, kPageSize); + return extractNativeLibFromApk(zipFile, zipEntry, fileName, nativeLibPath.c_str(), + when, uncompLen, crc); + } + + ALOGE("Library '%s' is not PAGE(%zu)-aligned - will not be able to open it directly " + "from apk.\n", + fileName, kPageSize); + return INSTALL_FAILED_INVALID_APK; + } + +#ifdef ENABLE_PUNCH_HOLES + // if library is uncompressed, punch hole in it in place + if (!punchHolesInElf64(zipFile->getZipFileName(), offset)) { + ALOGW("Failed to punch uncompressed elf file :%s inside apk : %s at offset: " + "%" PRIu64 "", + fileName, zipFile->getZipFileName(), offset); + } + + // if extra field for this zip file is present with some length, possibility is that it is + // padding added for zip alignment. Punch holes there too. + if (!punchHolesInZip(zipFile->getZipFileName(), offset, extraFieldLength)) { + ALOGW("Failed to punch apk : %s at extra field", zipFile->getZipFileName()); + } +#endif // ENABLE_PUNCH_HOLES + + return INSTALL_SUCCEEDED; + } + + return extractNativeLibFromApk(zipFile, zipEntry, fileName, nativeLibPath.c_str(), when, + uncompLen, crc); +} + +/* * An iterator over all shared libraries in a zip file. An entry is * considered to be a shared library if all of the conditions below are * satisfied : @@ -498,12 +520,24 @@ static int findSupportedAbi(JNIEnv* env, jlong apkHandle, jobjectArray supported return status; } +static inline bool app_compat_16kb_enabled() { + static const size_t kPageSize = getpagesize(); + + // App compat is only applicable on 16kb-page-size devices. + if (kPageSize != 0x4000) { + return false; + } + + return android::base::GetBoolProperty("bionic.linker.16kb.app_compat.enabled", false); +} + static jint com_android_internal_content_NativeLibraryHelper_copyNativeBinaries(JNIEnv *env, jclass clazz, jlong apkHandle, jstring javaNativeLibPath, jstring javaCpuAbi, jboolean extractNativeLibs, jboolean debuggable) { - void* args[] = { &javaNativeLibPath, &extractNativeLibs, &debuggable }; + jboolean app_compat_16kb = app_compat_16kb_enabled(); + void* args[] = { &javaNativeLibPath, &extractNativeLibs, &debuggable, &app_compat_16kb }; return (jint) iterateOverNativeFiles(env, apkHandle, javaCpuAbi, debuggable, copyFileIfChanged, reinterpret_cast<void*>(args)); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 17ff2ebc4f20..5decf7f7c2f7 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7648,7 +7648,8 @@ <permission android:name="android.permission.BIND_CARRIER_MESSAGING_CLIENT_SERVICE" android:protectionLevel="signature" /> - <!-- Must be required by an {@link android.service.watchdog.ExplicitHealthCheckService} to + <!-- @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY) @SystemApi + Must be required by an {@link android.service.watchdog.ExplicitHealthCheckService} to ensure that only the system can bind to it. @hide This is not a third-party API (intended for OEMs and system apps). --> diff --git a/core/res/res/drawable/ic_zen_priority_modes.xml b/core/res/res/drawable/ic_zen_priority_modes.xml index 98de27bb349f..e8691fce5a34 100644 --- a/core/res/res/drawable/ic_zen_priority_modes.xml +++ b/core/res/res/drawable/ic_zen_priority_modes.xml @@ -20,6 +20,6 @@ Copyright (C) 2024 The Android Open Source Project android:viewportHeight="960" android:tint="?android:attr/colorControlNormal"> <path - android:pathData="M160,480v-80h320v80L160,480ZM480,880q-80,0 -153.5,-29.5T196,764l56,-56q47,44 106,68t122,24q133,0 226.5,-93.5T800,480q0,-133 -93.5,-226.5T480,160v-80q83,0 155.5,31.5t127,86q54.5,54.5 86,127T880,480q0,82 -31.5,155t-86,127.5q-54.5,54.5 -127,86T480,880Z" - android:fillColor="@android:color/white"/> + android:fillColor="@android:color/white" + android:pathData="M480,720Q580,720 650,650Q720,580 720,480Q720,380 650,310Q580,240 480,240L480,480L310,650Q345,683 388.5,701.5Q432,720 480,720ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/> </vector> diff --git a/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java b/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java index 8685326a0173..ecd2f76a5160 100644 --- a/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java +++ b/core/tests/coretests/src/android/os/storage/StorageManagerIntegrationTest.java @@ -16,11 +16,8 @@ package android.os.storage; -import android.content.res.ObbInfo; -import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.ProxyFileDescriptorCallback; -import android.os.ServiceManager; import android.system.ErrnoException; import androidx.test.filters.LargeTest; @@ -107,14 +104,7 @@ public class StorageManagerIntegrationTest extends StorageManagerBaseTest { public void testMountBadPackageNameObb() throws Exception { final File file = createObbFile(OBB_FILE_3_BAD_PACKAGENAME, R.raw.obb_file3_bad_packagename); String filePath = file.getAbsolutePath(); - try { - mountObb(filePath, OnObbStateChangeListener.ERROR_PERMISSION_DENIED); - fail("mountObb should have thrown a exception as package name is incorrect"); - } catch (Exception ex) { - assertEquals("Path " + filePath - + " does not contain package name " + mContext.getPackageName(), - ex.getMessage()); - } + mountObb(filePath, OnObbStateChangeListener.ERROR_PERMISSION_DENIED); } /** @@ -164,48 +154,6 @@ public class StorageManagerIntegrationTest extends StorageManagerBaseTest { } } - @LargeTest - public void testObbInfo_withValidObbInfo_success() throws Exception { - final File file = createObbFile(OBB_FILE_1, R.raw.obb_file1); - String filePath = file.getAbsolutePath(); - try { - mountObb(filePath); - unmountObb(filePath, DONT_FORCE); - } catch (Exception ex) { - fail("No exception expected, got " + ex.getMessage()); - } - } - - @LargeTest - public void testObbInfo_withInvalidObbInfo_exception() throws Exception { - final File file = createObbFile(OBB_FILE_1, R.raw.obb_file1); - String rawPath = file.getAbsolutePath(); - String canonicalPath = file.getCanonicalPath(); - - ObbInfo obbInfo = ObbInfo.CREATOR.createFromParcel(Parcel.obtain()); - obbInfo.packageName = "com.android.obbcrash"; - obbInfo.version = 1; - obbInfo.filename = canonicalPath; - - try { - IStorageManager.Stub.asInterface(ServiceManager.getServiceOrThrow("mount")).mountObb( - rawPath, canonicalPath, new ObbActionListener(), 0, obbInfo); - fail("mountObb should have thrown a exception as package name is incorrect"); - } catch (SecurityException ex) { - assertEquals("Path " + canonicalPath - + " does not contain package name " + mContext.getPackageName(), - ex.getMessage()); - } - } - - private static class ObbActionListener extends IObbActionListener.Stub { - @SuppressWarnings("hiding") - @Override - public void onObbResult(String filename, int nonce, int status) { - - } - } - private static class MyThreadFactory implements ThreadFactory { Thread thread = null; diff --git a/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java index 4d9b591c0990..00ffda867d6a 100644 --- a/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java +++ b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java @@ -254,11 +254,8 @@ public class ImeBackAnimationControllerTest { float progress = 0.5f; mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT)); // verify correct ime insets manipulation - float interpolatedProgress = BACK_GESTURE.getInterpolation(progress); - int expectedInset = - (int) (IME_HEIGHT - interpolatedProgress * PEEK_FRACTION * IME_HEIGHT); verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha( - eq(Insets.of(0, 0, 0, expectedInset)), eq(1f), anyFloat()); + eq(Insets.of(0, 0, 0, getImeHeight(progress))), eq(1f), anyFloat()); } @Test @@ -268,12 +265,13 @@ public class ImeBackAnimationControllerTest { WindowInsetsAnimationControlListener animationControlListener = startBackGesture(); // progress back gesture - mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT)); + float progress = 0.5f; + mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT)); // commit back gesture mBackAnimationController.onBackInvoked(); - // verify setInsetsAndAlpha never called due onReady delayed + // verify setInsetsAndAlpha never called due to onReady delayed verify(mWindowInsetsAnimationController, never()).setInsetsAndAlpha(any(), anyInt(), anyFloat()); verify(mInsetsController, never()).setPredictiveBackImeHideAnimInProgress(eq(true)); @@ -283,7 +281,7 @@ public class ImeBackAnimationControllerTest { // verify setInsetsAndAlpha immediately called verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha( - eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat()); + eq(Insets.of(0, 0, 0, getImeHeight(progress))), eq(1f), anyFloat()); // verify post-commit hide anim has started verify(mInsetsController, times(1)).setPredictiveBackImeHideAnimInProgress(eq(true)); }); @@ -319,4 +317,9 @@ public class ImeBackAnimationControllerTest { return animationControlListener.getValue(); } + + private int getImeHeight(float gestureProgress) { + float interpolatedProgress = BACK_GESTURE.getInterpolation(gestureProgress); + return (int) (IME_HEIGHT - interpolatedProgress * PEEK_FRACTION * IME_HEIGHT); + } } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index ce7e85868e8c..bec8b1f76394 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -1022,7 +1022,7 @@ public class InsetsControllerTest { } @Test - public void testImeRequestedVisibleDuringPredictiveBackAnim() { + public void testImeRequestedVisibleDuringPredictiveBackAnimWithoutCallback() { prepareControls(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { // show ime as initial state @@ -1051,6 +1051,42 @@ public class InsetsControllerTest { } @Test + public void testImeRequestedInvisibleDuringPredictiveBackAnimWithCallback() { + prepareControls(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // set WindowInsetsAnimationCallback on ViewRoot + mViewRoot.getView().setWindowInsetsAnimationCallback( + new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { + @Override + public WindowInsets onProgress( + @NonNull WindowInsets insets, + @NonNull List<WindowInsetsAnimation> runningAnimations) { + return insets; + } + }); + + // show ime as initial state + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + mController.cancelExistingAnimations(); // fast forward show animation + assertTrue(mController.getState().peekSource(ID_IME).isVisible()); + + // start control request (for predictive back animation) + WindowInsetsAnimationControlListener listener = + mock(WindowInsetsAnimationControlListener.class); + mController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null, + listener, /*fromIme*/ false, /*duration*/ -1, /*interpolator*/ null, + ANIMATION_TYPE_USER, /*fromPredictiveBack*/ true); + + // Verify that onReady is called (after next predraw) + mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); + verify(listener).onReady(notNull(), eq(ime())); + + // verify that insets are requested invisible during animation + assertFalse(isRequestedVisible(mController, ime())); + }); + } + + @Test public void testImeShowRequestCancelsPredictiveBackPostCommitAnim() { prepareControls(); InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { @@ -1131,6 +1167,37 @@ public class InsetsControllerTest { }); } + @Test + public void testPredictiveBackControlRequestCancelledDuringImeHideAnim() { + prepareControls(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // show ime as initial state + if (!Flags.refactorInsetsController()) { + mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty()); + } else { + mController.show(ime(), false /* fromIme */, ImeTracker.Token.empty()); + } + mController.cancelExistingAnimations(); // fast forward show animation + mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw(); + assertTrue(mController.getState().peekSource(ID_IME).isVisible()); + + // start IME hide animation + mController.hide(ime(), true /* fromIme */, null /* statsToken */); + assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ime())); + + // start control request (for predictive back animation) + WindowInsetsAnimationControlListener listener = + mock(WindowInsetsAnimationControlListener.class); + mController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null, + listener, /*fromIme*/ false, /*duration*/ -1, /*interpolator*/ null, + ANIMATION_TYPE_USER, /*fromPredictiveBack*/ true); + + // verify that control request is cancelled and animation type remains HIDE + verify(listener).onCancelled(any()); + assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ime())); + }); + } + private void waitUntilNextFrame() throws Exception { final CountDownLatch latch = new CountDownLatch(1); Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT, diff --git a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java index c3a5b19c9442..499caf5e12d3 100644 --- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java +++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java @@ -359,7 +359,7 @@ public class FrameTrackerTest { tracker.end(FrameTracker.REASON_END_NORMAL); // Send incomplete callback for 102L - sendSfFrame(tracker, 4, 102L, JANK_NONE); + sendSfFrame(tracker, 102L, JANK_NONE); // Send janky but complete callbck fo 103L sendFrame(tracker, 50, JANK_APP_DEADLINE_MISSED, 103L); @@ -629,7 +629,7 @@ public class FrameTrackerTest { if (!tracker.mSurfaceOnly) { sendHwuiFrame(tracker, durationMillis, vsyncId, firstWindowFrame); } - sendSfFrame(tracker, durationMillis, vsyncId, jankType); + sendSfFrame(tracker, vsyncId, jankType); } private void sendHwuiFrame(FrameTracker tracker, long durationMillis, long vsyncId, @@ -645,13 +645,11 @@ public class FrameTrackerTest { captor.getValue().run(); } - private void sendSfFrame( - FrameTracker tracker, long durationMillis, long vsyncId, @JankType int jankType) { + private void sendSfFrame(FrameTracker tracker, long vsyncId, @JankType int jankType) { final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); doNothing().when(tracker).postCallback(captor.capture()); mListenerCapture.getValue().onJankDataAvailable(new JankData[] { - new JankData(vsyncId, jankType, FRAME_TIME_60Hz, FRAME_TIME_60Hz, - TimeUnit.MILLISECONDS.toNanos(durationMillis)) + new JankData(vsyncId, jankType, FRAME_TIME_60Hz) }); captor.getValue().run(); } 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 5657647ff5c0..f2f2b7ea7174 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -58,18 +58,22 @@ import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; +import android.app.AppGlobals; import android.app.Application; import android.app.Instrumentation; import android.app.servertransaction.ClientTransactionListenerController; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemProperties; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -116,7 +120,7 @@ import java.util.function.BiConsumer; public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, ActivityEmbeddingComponent, DividerPresenter.DragEventCallback { static final String TAG = "SplitController"; - static final boolean ENABLE_SHELL_TRANSITIONS = true; + static final boolean ENABLE_SHELL_TRANSITIONS = getShellTransitEnabled(); // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without // association. It's not set in WM Extensions nor Wm Jetpack library currently. @@ -920,7 +924,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Update all TaskFragments in the Task. Make a copy of the list since some may be // removed on updating. - final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers(); + final List<TaskFragmentContainer> containers + = new ArrayList<>(taskContainer.getTaskFragmentContainers()); for (int i = containers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = containers.get(i); // Wait until onTaskFragmentAppeared to update new container. @@ -3308,4 +3313,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen transactionRecord.apply(false /* shouldApplyIndependently */); } } + + // TODO(b/207070762): cleanup with legacy app transition + private static boolean getShellTransitEnabled() { + try { + if (AppGlobals.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE, 0)) { + return SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); + } + } catch (RemoteException re) { + Log.w(TAG, "Error getting system features"); + } + return true; + } } diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index b338a2ae2b79..a79bc97c440c 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -39,17 +39,6 @@ filegroup { path: "src", } -// Sources that have no dependencies that can be used directly downstream of this library -// TODO(b/322791067): move these sources to WindowManager-Shell-shared -filegroup { - name: "wm_shell_util-sources", - srcs: [ - "src/com/android/wm/shell/common/bubbles/*.kt", - "src/com/android/wm/shell/common/bubbles/*.java", - ], - path: "src", -} - // Aidls which can be used directly downstream of this library filegroup { name: "wm_shell-aidls", @@ -184,9 +173,11 @@ java_library { ":wm_shell-shared-aidls", ], static_libs: [ + "androidx.core_core-animation", "androidx.dynamicanimation_dynamicanimation", "jsr330", ], + kotlincflags: ["-Xjvm-default=all"], } java_library { @@ -212,7 +203,6 @@ android_library { ], static_libs: [ "androidx.appcompat_appcompat", - "androidx.core_core-animation", "androidx.core_core-ktx", "androidx.arch.core_core-runtime", "androidx.datastore_datastore", diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 9de10c0619da..470b7a281ed8 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -138,3 +138,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_bubble_to_fullscreen" + namespace: "multitasking" + description: "Enable an option to move bubbles to fullscreen" + bug: "363326492" +} diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt index d35f493a8f60..f09969d253d3 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.bubbles import android.view.LayoutInflater -import com.android.wm.shell.common.bubbles.BubblePopupView +import com.android.wm.shell.shared.bubbles.BubblePopupView import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager import com.android.wm.shell.R import org.junit.Rule diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index 4b97451a0c41..b38d00da6dfa 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -30,7 +30,7 @@ import androidx.test.filters.SmallTest import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor import org.junit.Before diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index faadf1d623c9..96ffa03a1f65 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -53,7 +53,7 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleBarLocation import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt index 935d12916f56..ecb2b25a02f1 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -31,12 +31,12 @@ import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.bubbles.DeviceConfig -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION -import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION -import com.android.wm.shell.common.bubbles.BubbleBarLocation -import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml index a0a06f1b3721..806d026a7e7c 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.wm.shell.common.bubbles.BubblePopupView +<com.android.wm.shell.shared.bubbles.BubblePopupView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -53,4 +53,4 @@ android:textAlignment="center" android:text="@string/bubble_bar_education_manage_text"/> -</com.android.wm.shell.common.bubbles.BubblePopupView>
\ No newline at end of file +</com.android.wm.shell.shared.bubbles.BubblePopupView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml index b489a5c1acd0..7fa586c626be 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.wm.shell.common.bubbles.BubblePopupView +<com.android.wm.shell.shared.bubbles.BubblePopupView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -53,4 +53,4 @@ android:textAlignment="center" android:text="@string/bubble_bar_education_stack_text"/> -</com.android.wm.shell.common.bubbles.BubblePopupView>
\ No newline at end of file +</com.android.wm.shell.shared.bubbles.BubblePopupView>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 36d0a3c63b03..a353db72b914 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -155,6 +155,8 @@ <string name="bubbles_app_settings"><xliff:g id="notification_title" example="Android Messages">%1$s</xliff:g> settings</string> <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=30] --> <string name="bubble_dismiss_text">Dismiss bubble</string> + <!-- Text used to move the bubble to fullscreen. [CHAR LIMIT=30] --> + <string name="bubble_fullscreen_text">Move to fullscreen</string> <!-- Button text to stop a conversation from bubbling [CHAR LIMIT=60]--> <string name="bubbles_dont_bubble_conversation">Don\u2019t bubble conversation</string> <!-- Title text for the bubbles feature education cling shown when a bubble is on screen for the first time. [CHAR LIMIT=60]--> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt index eec24683db8a..7086691e7431 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.graphics.Point import android.graphics.RectF @@ -23,9 +23,9 @@ import androidx.annotation.VisibleForTesting import androidx.core.animation.Animator import androidx.core.animation.AnimatorListenerAdapter import androidx.core.animation.ObjectAnimator -import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener -import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT /** * Base class for common logic shared between different bubble views to support pinning bubble bar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.aidl index 3c5beeb48806..4fe76115fa0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.aidl @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; parcelable BubbleBarLocation;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt index f0bdfdef1073..191875d38daf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.os.Parcel import android.os.Parcelable diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarUpdate.java index ec3c6013e544..5bde1e8fae3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarUpdate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.annotation.NonNull; import android.annotation.Nullable; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleConstants.java index 0329b8df7544..3396bc441467 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; /** * Constants shared between bubbles in shell & things we have to do for bubbles in launcher. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleInfo.java index 829af08e612a..58766826bd3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleInfo.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.annotation.NonNull; import android.annotation.Nullable; @@ -48,10 +48,11 @@ public class BubbleInfo implements Parcelable { @Nullable private String mAppName; private boolean mIsImportantConversation; + private boolean mShowAppBadge; public BubbleInfo(String key, int flags, @Nullable String shortcutId, @Nullable Icon icon, int userId, String packageName, @Nullable String title, @Nullable String appName, - boolean isImportantConversation) { + boolean isImportantConversation, boolean showAppBadge) { mKey = key; mFlags = flags; mShortcutId = shortcutId; @@ -61,6 +62,7 @@ public class BubbleInfo implements Parcelable { mTitle = title; mAppName = appName; mIsImportantConversation = isImportantConversation; + mShowAppBadge = showAppBadge; } private BubbleInfo(Parcel source) { @@ -73,6 +75,7 @@ public class BubbleInfo implements Parcelable { mTitle = source.readString(); mAppName = source.readString(); mIsImportantConversation = source.readBoolean(); + mShowAppBadge = source.readBoolean(); } public String getKey() { @@ -115,6 +118,10 @@ public class BubbleInfo implements Parcelable { return mIsImportantConversation; } + public boolean showAppBadge() { + return mShowAppBadge; + } + /** * Whether this bubble is currently being hidden from the stack. */ @@ -172,6 +179,7 @@ public class BubbleInfo implements Parcelable { parcel.writeString(mTitle); parcel.writeString(mAppName); parcel.writeBoolean(mIsImportantConversation); + parcel.writeBoolean(mShowAppBadge); } @NonNull diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupDrawable.kt index 887af17c9653..8681acf93ab3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupDrawable.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupDrawable.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.annotation.ColorInt import android.graphics.Canvas diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupView.kt index 444fbf7884be..802d7d131d95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubblePopupView.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubblePopupView.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.content.Context import android.graphics.Rect diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissCircleView.java index 7c5bb211a4cc..0c051560f714 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissCircleView.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissCircleView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.content.Context; import android.content.res.Configuration; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissView.kt index e06de9e9353c..2bb66b0bbcd3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DismissView.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.animation.ObjectAnimator import android.content.Context diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/OWNERS index 08c70314973e..08c70314973e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/OWNERS diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RelativeTouchListener.kt index 4e55ba23407b..b1f4e331a98d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RelativeTouchListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.graphics.PointF import android.view.MotionEvent diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RemovedBubble.java index f90591b84b7e..c83696c01613 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RemovedBubble.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/RemovedBubble.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles; +package com.android.wm.shell.shared.bubbles; import android.annotation.NonNull; import android.os.Parcel; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java index 9999f08ee03c..b92b8ef657a3 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/navigationbar/RegionSamplingHelper.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shared.navigationbar; +package com.android.wm.shell.shared.handles; import static android.view.Display.DEFAULT_DISPLAY; @@ -194,7 +194,7 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, ViewRootImpl viewRootImpl = mSampledView.getViewRootImpl(); SurfaceControl stopLayerControl = null; if (viewRootImpl != null) { - stopLayerControl = viewRootImpl.getSurfaceControl(); + stopLayerControl = viewRootImpl.getSurfaceControl(); } if (stopLayerControl == null || !stopLayerControl.isValid()) { if (!mWaitingOnDraw) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 33949f5d8d5f..ef679dae0157 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -74,6 +74,7 @@ import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; @@ -1272,19 +1273,24 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ComponentName openComponent = null; int tmpSize; int openTaskId = INVALID_TASK_ID; + WindowContainerToken openToken = null; for (int j = init.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = init.getChanges().get(j); if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { openComponent = findComponentName(change); openTaskId = findTaskId(change); + openToken = findToken(change); if (change.hasFlags(FLAG_SHOW_WALLPAPER)) { openShowWallpaper = true; } break; } } - if (openComponent == null && openTaskId == INVALID_TASK_ID) { - // shouldn't happen. + if (openComponent == null && openTaskId == INVALID_TASK_ID && openToken == null) { + // This shouldn't happen, but if that happen, consume the initial transition anyway. + Log.e(TAG, "Unable to merge following transition, cannot find the gesture " + + "animated target from the open transition=" + mOpenTransitionInfo); + mOpenTransitionInfo = null; return; } // find first non-prepare open target @@ -1315,7 +1321,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont boolean moveToTop = false; for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isSameChangeTarget(openComponent, openTaskId, change)) { + if (isSameChangeTarget(openComponent, openTaskId, openToken, change)) { moveToTop = change.hasFlags(FLAG_MOVED_TO_TOP); info.getChanges().remove(j); } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER)) @@ -1329,7 +1335,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont for (int i = 0; i < tmpSize; ++i) { final TransitionInfo.Change change = init.getChanges().get(i); if (moveToTop) { - if (isSameChangeTarget(openComponent, openTaskId, change)) { + if (isSameChangeTarget(openComponent, openTaskId, openToken, change)) { change.setFlags(change.getFlags() | FLAG_MOVED_TO_TOP); } } @@ -1358,7 +1364,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (nonBackClose && nonBackOpen) { for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isSameChangeTarget(openComponent, openTaskId, change)) { + if (isSameChangeTarget(openComponent, openTaskId, openToken, change)) { info.getChanges().remove(j); } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { info.getChanges().remove(j); @@ -1368,6 +1374,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation transition, merge pending " + "transitions result=%s", info); + // Only handle one merge transition request. + mOpenTransitionInfo = null; } @Override @@ -1378,7 +1386,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mClosePrepareTransition = null; } // try to handle unexpected transition - mergePendingTransitions(info); + if (mOpenTransitionInfo != null) { + mergePendingTransitions(info); + } if (isNotGestureBackTransition(info) || shouldCancelAnimation(info) || !mCloseTransitionRequested) { @@ -1628,6 +1638,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return false; } + private static WindowContainerToken findToken(TransitionInfo.Change change) { + return change.getContainer(); + } + private static ComponentName findComponentName(TransitionInfo.Change change) { final ComponentName componentName = change.getActivityComponent(); if (componentName != null) { @@ -1649,11 +1663,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } private static boolean isSameChangeTarget(ComponentName topActivity, int taskId, - TransitionInfo.Change change) { + WindowContainerToken token, TransitionInfo.Change change) { final ComponentName openChange = findComponentName(change); final int firstTaskId = findTaskId(change); + final WindowContainerToken openToken = findToken(change); return (openChange != null && openChange == topActivity) - || (firstTaskId != INVALID_TASK_ID && firstTaskId == taskId); + || (firstTaskId != INVALID_TASK_ID && firstTaskId == taskId) + || (openToken != null && token == openToken); } private static boolean canBeTransitionTarget(TransitionInfo.Change change) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 3e758bbad29b..0c95934abf93 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -53,9 +53,9 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; -import com.android.wm.shell.common.bubbles.BubbleInfo; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleInfo; import java.io.PrintWriter; import java.util.List; @@ -349,7 +349,8 @@ public class Bubble implements BubbleViewProvider { getPackageName(), getTitle(), getAppName(), - isImportantConversation()); + isImportantConversation(), + !isAppLaunchIntent()); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index b508c1ba7fe4..c545d73734f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -104,14 +104,14 @@ import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 4ad1802cba7f..709a7bdc61f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -41,10 +41,10 @@ import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; -import com.android.wm.shell.common.bubbles.RemovedBubble; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; +import com.android.wm.shell.shared.bubbles.RemovedBubble; import java.io.PrintWriter; import java.util.ArrayList; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index 4e80e903b522..ec4854b47aff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.bubbles -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** Manager interface for bubble expanded views. */ interface BubbleExpandedViewManager { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt index bdb09e11d5ad..fd110a276826 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePopupViewExt.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.bubbles import android.graphics.Color import com.android.wm.shell.R -import com.android.wm.shell.common.bubbles.BubblePopupDrawable -import com.android.wm.shell.common.bubbles.BubblePopupView +import com.android.wm.shell.shared.bubbles.BubblePopupDrawable +import com.android.wm.shell.shared.bubbles.BubblePopupView /** * A convenience method to setup the [BubblePopupView] with the correct config using local resources diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index 0cf187bd9c0f..c386c9398624 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -32,7 +32,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 53bbf888df5a..2795881f0938 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -24,10 +24,10 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; import static com.android.wm.shell.bubbles.BubblePositioner.StackPinnedEdge.LEFT; import static com.android.wm.shell.bubbles.BubblePositioner.StackPinnedEdge.RIGHT; -import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN; import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_OUT; +import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -91,8 +91,8 @@ import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout; import com.android.wm.shell.bubbles.animation.StackAnimationController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.DismissView; -import com.android.wm.shell.common.bubbles.RelativeTouchListener; +import com.android.wm.shell.shared.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.RelativeTouchListener; import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.animation.PhysicsAnimator; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 9a27fb65ac2c..62895fe7c7cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -38,9 +38,9 @@ import android.window.ScreenCapture.SynchronousScreenCaptureListener; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import java.lang.annotation.Retention; import java.lang.annotation.Target; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt index 48692d41016e..00a81727a9ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissViewExt.kt @@ -18,7 +18,7 @@ package com.android.wm.shell.bubbles import com.android.wm.shell.R -import com.android.wm.shell.common.bubbles.DismissView +import com.android.wm.shell.shared.bubbles.DismissView fun DismissView.setup() { setup(DismissView.Config( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 5779a8f7bcc4..1855b938f48e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -20,7 +20,7 @@ import android.content.Intent; import android.graphics.Rect; import android.content.pm.ShortcutInfo; import com.android.wm.shell.bubbles.IBubblesListener; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl index 14d29cd887bb..eb907dbb6597 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl @@ -17,7 +17,7 @@ package com.android.wm.shell.bubbles; import android.os.Bundle; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; /** * Listener interface that Launcher attaches to SystemUI to get bubbles callbacks. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 6d868d215482..f90b2aa95555 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -46,7 +46,7 @@ import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.taskview.TaskView; import java.util.function.Supplier; @@ -228,6 +228,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView public void onDismissBubble(Bubble bubble) { mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE); } + + @Override + public void onMoveToFullscreen(Bubble bubble) { + if (mTaskView != null) { + mTaskView.moveToFullscreen(); + } + } }); mHandleView.setOnClickListener(view -> { mMenuViewController.showMenu(true /* animated */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index eeb5c94c8f81..07463bb024a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -20,8 +20,8 @@ import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.common.bubbles.DismissView -import com.android.wm.shell.common.bubbles.RelativeTouchListener +import com.android.wm.shell.shared.bubbles.DismissView +import com.android.wm.shell.shared.bubbles.RelativeTouchListener import com.android.wm.shell.shared.magnetictarget.MagnetizedObject /** Controller for handling drag interactions with [BubbleBarExpandedView] */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index ac424532e87b..1c9c195cf718 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -44,9 +44,9 @@ import com.android.wm.shell.bubbles.BubbleViewProvider; import com.android.wm.shell.bubbles.DeviceConfig; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener; -import com.android.wm.shell.common.bubbles.BaseBubblePinController; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.BaseBubblePinController; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.DismissView; import kotlin.Unit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 0d72998eb2e8..514810745e10 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -29,6 +29,7 @@ import android.view.ViewGroup; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; +import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.shared.animation.PhysicsAnimator; @@ -219,6 +220,21 @@ class BubbleBarMenuViewController { } )); + if (Flags.enableBubbleAnything() || Flags.enableBubbleToFullscreen()) { + menuActions.add(new BubbleBarMenuView.MenuAction( + Icon.createWithResource(resources, + R.drawable.desktop_mode_ic_handle_menu_fullscreen), + resources.getString(R.string.bubble_fullscreen_text), + tintColor, + view -> { + hideMenu(true /* animated */); + if (mListener != null) { + mListener.onMoveToFullscreen(bubble); + } + } + )); + } + return menuActions; } @@ -249,5 +265,10 @@ class BubbleBarMenuViewController { * Dismiss bubble and remove it from the bubble stack */ void onDismissBubble(Bubble bubble); + + /** + * Move the bubble to fullscreen. + */ + void onMoveToFullscreen(Bubble bubble); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt index e108f7be48c7..9fd255ded0ad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt @@ -34,9 +34,9 @@ import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME import com.android.wm.shell.bubbles.BubbleEducationController import com.android.wm.shell.bubbles.BubbleViewProvider import com.android.wm.shell.bubbles.setup -import com.android.wm.shell.common.bubbles.BubblePopupDrawable -import com.android.wm.shell.common.bubbles.BubblePopupView import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.BubblePopupDrawable +import com.android.wm.shell.shared.bubbles.BubblePopupView import kotlin.math.roundToInt /** Manages bubble education presentation and animation */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt index 651bf022e07d..23ba2bff5ebc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt @@ -25,8 +25,8 @@ import android.widget.FrameLayout import androidx.core.view.updateLayoutParams import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.common.bubbles.BaseBubblePinController -import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.BaseBubblePinController +import com.android.wm.shell.shared.bubbles.BubbleBarLocation /** * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java index fad3dee1f927..1929729eb1ad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ScreenshotUtils.java @@ -42,6 +42,7 @@ public class ScreenshotUtils { .setSourceCrop(crop) .setCaptureSecureLayers(true) .setAllowProtected(true) + .setHintForSeamlessTransition(true) .build())); } @@ -78,6 +79,9 @@ public class ScreenshotUtils { mTransaction.setColorSpace(mScreenshot, buffer.getColorSpace()); mTransaction.reparent(mScreenshot, mParentSurfaceControl); mTransaction.setLayer(mScreenshot, mLayer); + if (buffer.containsHdrLayers()) { + mTransaction.setDimmingEnabled(mScreenshot, false); + } mTransaction.show(mScreenshot); mTransaction.apply(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 42937c134e7f..4adea233b734 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -148,7 +148,11 @@ import java.util.function.IntPredicate; * dependencies that are device/form factor SystemUI implementation specific should go into their * respective modules (ie. {@link WMShellModule} for handheld, {@link TvWMShellModule} for tv, etc.) */ -@Module(includes = WMShellConcurrencyModule.class) +@Module( + includes = { + WMShellConcurrencyModule.class, + WMShellCoroutinesModule.class + }) public abstract class WMShellBaseModule { // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 46cb6ec36196..02ecfd983d73 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -73,6 +73,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator; import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; +import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.draganddrop.DragAndDropController; @@ -118,6 +119,8 @@ import dagger.Lazy; import dagger.Module; import dagger.Provides; +import kotlinx.coroutines.CoroutineScope; + import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -743,6 +746,17 @@ public abstract class WMShellModule { return new AppHandleEducationFilter(context, appHandleEducationDatastoreRepository); } + @WMSingleton + @Provides + static AppHandleEducationController provideAppHandleEducationController( + AppHandleEducationFilter appHandleEducationFilter, + ShellTaskOrganizer shellTaskOrganizer, + AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository, + @ShellMainThread CoroutineScope applicationScope) { + return new AppHandleEducationController(appHandleEducationFilter, + shellTaskOrganizer, appHandleEducationDatastoreRepository, applicationScope); + } + // // Drag and drop // @@ -784,7 +798,8 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, + AppHandleEducationController appHandleEducationController ) { return new Object(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 90f8276240a7..1d16980c617d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -734,17 +734,33 @@ class DesktopTasksController( * Quick-resize to the right or left half of the stable bounds. * * @param taskInfo current task that is being snap-resized via dragging or maximize menu button + * @param taskSurface the leash of the task being dragged * @param currentDragBounds current position of the task leash being dragged (or current task * bounds if being snapped resize via maximize menu button) * @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to. */ fun snapToHalfScreen( taskInfo: RunningTaskInfo, + taskSurface: SurfaceControl, currentDragBounds: Rect, position: SnapPosition ) { val destinationBounds = getSnapBounds(taskInfo, position) - if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return + if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) { + // Handle the case where we attempt to snap resize when already snap resized: the task + // position won't need to change but we want to animate the surface going back to the + // snapped position from the "dragged-to-the-edge" position. + if (destinationBounds != currentDragBounds) { + returnToDragStartAnimator.start( + taskInfo.taskId, + taskSurface, + startBounds = currentDragBounds, + endBounds = destinationBounds, + isResizable = taskInfo.isResizeable + ) + } + return + } taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true) val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) @@ -774,13 +790,14 @@ class DesktopTasksController( taskInfo.taskId, taskSurface, startBounds = currentDragBounds, - endBounds = dragStartBounds + endBounds = dragStartBounds, + isResizable = taskInfo.isResizeable, ) } else { interactionJankMonitor.begin( taskSurface, context, CUJ_DESKTOP_MODE_SNAP_RESIZE, "drag_resizable" ) - snapToHalfScreen(taskInfo, currentDragBounds, position) + snapToHalfScreen(taskInfo, taskSurface, currentDragBounds, position) } } @@ -896,6 +913,7 @@ class DesktopTasksController( val intent = Intent(context, DesktopWallpaperActivity::class.java) val options = ActivityOptions.makeBasic().apply { + launchWindowingMode = WINDOWING_MODE_FULLSCREEN pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt index 4c5258f2bfcd..f4df42cde10f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt @@ -48,7 +48,13 @@ class ReturnToDragStartAnimator( } /** Builds new animator and starts animation of task leash reposition. */ - fun start(taskId: Int, taskSurface: SurfaceControl, startBounds: Rect, endBounds: Rect) { + fun start( + taskId: Int, + taskSurface: SurfaceControl, + startBounds: Rect, + endBounds: Rect, + isResizable: Boolean + ) { val tx = transactionSupplier.get() boundsAnimator?.cancel() @@ -81,11 +87,13 @@ class ReturnToDragStartAnimator( .apply() taskRepositionAnimationListener.onAnimationEnd(taskId) boundsAnimator = null - Toast.makeText( - context, - R.string.desktop_mode_non_resizable_snap_text, - Toast.LENGTH_SHORT - ).show() + if (!isResizable) { + Toast.makeText( + context, + R.string.desktop_mode_non_resizable_snap_text, + Toast.LENGTH_SHORT + ).show() + } interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE) } ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt new file mode 100644 index 000000000000..6013e97977d8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 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.desktopmode.education + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.os.SystemProperties +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository +import com.android.wm.shell.shared.annotations.ShellMainThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Controls app handle education end to end. + * + * Listen to the user trigger for app handle education, calls an api to check if the education + * should be shown and calls an api to show education. + */ +@OptIn(kotlinx.coroutines.FlowPreview::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class AppHandleEducationController( + private val appHandleEducationFilter: AppHandleEducationFilter, + shellTaskOrganizer: ShellTaskOrganizer, + private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository, + @ShellMainThread private val applicationCoroutineScope: CoroutineScope +) { + init { + runIfEducationFeatureEnabled { + // TODO: b/361038716 - Use app handle state flow instead of focus task change flow + val focusTaskChangeFlow = focusTaskChangeFlow(shellTaskOrganizer) + applicationCoroutineScope.launch { + // Central block handling the app's educational flow end-to-end. + // This flow listens to the changes to the result of + // [WindowingEducationProto#hasEducationViewedTimestampMillis()] in datastore proto object + isEducationViewedFlow() + .flatMapLatest { isEducationViewed -> + if (isEducationViewed) { + // If the education is viewed then return emptyFlow() that completes immediately. + // This will help us to not listen to focus task changes after the education has + // been viewed already. + emptyFlow() + } else { + // This flow listens for focus task changes, which trigger the app handle education. + focusTaskChangeFlow + .filter { runningTaskInfo -> + runningTaskInfo.topActivityInfo?.packageName?.let { + appHandleEducationFilter.shouldShowAppHandleEducation(it) + } ?: false && runningTaskInfo.windowingMode != WINDOWING_MODE_FREEFORM + } + .distinctUntilChanged() + } + } + .debounce( + APP_HANDLE_EDUCATION_DELAY) // Wait for few seconds, if the focus task changes. + // During the delay then current emission will be cancelled. + .flowOn(Dispatchers.IO) + .collectLatest { + // Fire and forget show education suspend function, manage entire lifecycle of + // tooltip in UI class. + } + } + } + } + + private inline fun runIfEducationFeatureEnabled(block: () -> Unit) { + if (Flags.enableDesktopWindowingAppHandleEducation()) block() + } + + private fun isEducationViewedFlow(): Flow<Boolean> = + appHandleEducationDatastoreRepository.dataStoreFlow + .map { preferences -> preferences.hasEducationViewedTimestampMillis() } + .distinctUntilChanged() + + private fun focusTaskChangeFlow(shellTaskOrganizer: ShellTaskOrganizer): Flow<RunningTaskInfo> = + callbackFlow { + val focusTaskChange = ShellTaskOrganizer.FocusListener { taskInfo -> trySend(taskInfo) } + shellTaskOrganizer.addFocusListener(focusTaskChange) + awaitClose { shellTaskOrganizer.removeFocusListener(focusTaskChange) } + } + + private companion object { + val APP_HANDLE_EDUCATION_DELAY: Long + get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt index a7fff8af99fa..f420c5be456f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt @@ -25,9 +25,12 @@ import androidx.datastore.core.Serializer import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting +import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.time.Duration +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first /** @@ -46,17 +49,26 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { serializer = WindowingEducationProtoSerializer, produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) })) + /** Provides dataStore.data flow and handles exceptions thrown during collection */ + val dataStoreFlow: Flow<WindowingEducationProto> = + dataStore.data.catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + Log.e( + TAG, + "Error in reading app handle education related data from datastore, data is " + + "stored in a file named $APP_HANDLE_EDUCATION_DATASTORE_FILEPATH", + exception) + } else { + throw exception + } + } + /** * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the * DataStore is empty or there's an error reading, it returns the default value of Proto. */ - suspend fun windowingEducationProto(): WindowingEducationProto = - try { - dataStore.data.first() - } catch (e: Exception) { - Log.e(TAG, "Unable to read from datastore") - WindowingEducationProto.getDefaultInstance() - } + suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() /** * Updates [AppHandleEducation.appUsageStats] and diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index 0acc7df98d1c..faa97ac4512f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -98,9 +98,8 @@ Don't: ### Exposing shared code for use in Launcher Launcher doesn't currently build against the Shell library, but needs to have access to some shared AIDL interfaces and constants. Currently, all AIDL files, and classes under the -`com.android.wm.shell.util` package are automatically built into the `SystemUISharedLib` that +`com.android.wm.shell.shared` package are automatically built into the `SystemUISharedLib` that Launcher uses. -If the new code doesn't fall into those categories, they can be added explicitly in the Shell's -[Android.bp](/libs/WindowManager/Shell/Android.bp) file under the -`wm_shell_util-sources` filegroup.
\ No newline at end of file +If the new code doesn't fall into those categories, they should be moved to the Shell shared +package (`com.android.wm.shell.shared`) under the `WindowManager-Shell-shared` library.
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java index 0d2b8e70422d..06d231144d81 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -35,9 +35,9 @@ import androidx.annotation.NonNull; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.DismissCircleView; -import com.android.wm.shell.common.bubbles.DismissView; import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.shared.bubbles.DismissCircleView; +import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import kotlin.Unit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java index 8a9302bcfc98..8ebdc96c21a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterExitAnimator.java @@ -22,6 +22,7 @@ import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; import android.graphics.Rect; +import android.view.Surface; import android.view.SurfaceControl; import androidx.annotation.NonNull; @@ -51,8 +52,10 @@ public class PipEnterExitAnimator extends ValueAnimator @NonNull private final SurfaceControl mLeash; private final SurfaceControl.Transaction mStartTransaction; - private final int mEnterAnimationDuration; + private final SurfaceControl.Transaction mFinishTransaction; + private final int mEnterExitAnimationDuration; private final @BOUNDS int mDirection; + private final @Surface.Rotation int mRotation; // optional callbacks for tracking animation start and end @Nullable private Runnable mAnimationStartCallback; @@ -62,37 +65,59 @@ public class PipEnterExitAnimator extends ValueAnimator private final Rect mStartBounds = new Rect(); private final Rect mEndBounds = new Rect(); + @Nullable private final Rect mSourceRectHint; + private final Rect mSourceRectHintInsets = new Rect(); + private final Rect mZeroInsets = new Rect(0, 0, 0, 0); + // Bounds updated by the evaluator as animator is running. private final Rect mAnimatedRect = new Rect(); private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; private final RectEvaluator mRectEvaluator; + private final RectEvaluator mInsetEvaluator; private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; public PipEnterExitAnimator(Context context, @NonNull SurfaceControl leash, SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, @NonNull Rect baseBounds, @NonNull Rect startBounds, @NonNull Rect endBounds, - @BOUNDS int direction) { + @Nullable Rect sourceRectHint, + @BOUNDS int direction, + @Surface.Rotation int rotation) { mLeash = leash; mStartTransaction = startTransaction; + mFinishTransaction = finishTransaction; mBaseBounds.set(baseBounds); mStartBounds.set(startBounds); mAnimatedRect.set(startBounds); mEndBounds.set(endBounds); mRectEvaluator = new RectEvaluator(mAnimatedRect); + mInsetEvaluator = new RectEvaluator(new Rect()); mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(context); mDirection = direction; + mRotation = rotation; + + mSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) : null; + if (mSourceRectHint != null) { + mSourceRectHintInsets.set( + mSourceRectHint.left - mBaseBounds.left, + mSourceRectHint.top - mBaseBounds.top, + mBaseBounds.right - mSourceRectHint.right, + mBaseBounds.bottom - mSourceRectHint.bottom + ); + } mSurfaceControlTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); - mEnterAnimationDuration = context.getResources() + mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipEnterAnimationDuration); - setDuration(mEnterAnimationDuration); + setObjectValues(startBounds, endBounds); + setDuration(mEnterExitAnimationDuration); setEvaluator(mRectEvaluator); addListener(this); addUpdateListener(this); @@ -118,6 +143,14 @@ public class PipEnterExitAnimator extends ValueAnimator @Override public void onAnimationEnd(@NonNull Animator animation) { + if (mFinishTransaction != null) { + // finishTransaction might override some state (eg. corner radii) so we want to + // manually set the state to the end of the animation + mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, mSourceRectHint, + mBaseBounds, mAnimatedRect, getInsets(1f), isInPipDirection(), 1f) + .round(mFinishTransaction, mLeash, isInPipDirection()) + .shadow(mFinishTransaction, mLeash, isInPipDirection()); + } if (mAnimationEndCallback != null) { mAnimationEndCallback.run(); } @@ -127,19 +160,32 @@ public class PipEnterExitAnimator extends ValueAnimator public void onAnimationUpdate(@NonNull ValueAnimator animation) { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); final float fraction = getAnimatedFraction(); + Rect insets = getInsets(fraction); + // TODO (b/350801661): implement fixed rotation - mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, null, - mBaseBounds, mAnimatedRect, null, isInPipDirection(), fraction) + mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, + mBaseBounds, mAnimatedRect, insets, isInPipDirection(), fraction) .round(tx, mLeash, isInPipDirection()) .shadow(tx, mLeash, isInPipDirection()); tx.apply(); } + private Rect getInsets(float fraction) { + Rect startInsets = isInPipDirection() ? mZeroInsets : mSourceRectHintInsets; + Rect endInsets = isInPipDirection() ? mSourceRectHintInsets : mZeroInsets; + + return mInsetEvaluator.evaluate(fraction, startInsets, endInsets); + } + private boolean isInPipDirection() { return mDirection == BOUNDS_ENTER; } + private boolean isOutPipDirection() { + return mDirection == BOUNDS_EXIT; + } + // no-ops @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java index e04178e6d58c..b3070f29c6e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java @@ -35,9 +35,9 @@ import androidx.annotation.NonNull; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.DismissCircleView; -import com.android.wm.shell.common.bubbles.DismissView; import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.shared.bubbles.DismissCircleView; +import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import kotlin.Unit; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java index 7f168800fb29..262c14d2bfe3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java @@ -25,6 +25,7 @@ import android.graphics.Rect; import android.os.Bundle; import android.view.SurfaceControl; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.util.Preconditions; @@ -88,6 +89,11 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, : new PictureInPictureParams.Builder().build()); } + @NonNull + public PictureInPictureParams getPictureInPictureParams() { + return mPictureInPictureParams; + } + @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { PictureInPictureParams params = taskInfo.pictureInPictureParams; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 44baabdd5e2e..f93233ec7461 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -36,6 +36,7 @@ import android.content.Context; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.view.Surface; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -398,17 +399,22 @@ public class PipTransition extends PipTransitionController implements SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition."); + Rect sourceRectHint = null; + if (pipChange.getTaskInfo() != null + && pipChange.getTaskInfo().pictureInPictureParams != null) { + sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint(); + } + PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash, - startTransaction, startBounds, startBounds, endBounds, - PipEnterExitAnimator.BOUNDS_ENTER); + startTransaction, finishTransaction, startBounds, startBounds, endBounds, + sourceRectHint, PipEnterExitAnimator.BOUNDS_ENTER, Surface.ROTATION_0); tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), this::onClientDrawAtTransitionEnd); finishWct.setBoundsChangeTransaction(pipTaskToken, tx); - animator.setAnimationEndCallback(() -> { - finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct); - }); + animator.setAnimationEndCallback(() -> + finishCallback.onTransitionFinished(finishWct)); animator.start(); return true; @@ -452,19 +458,53 @@ public class PipTransition extends PipTransitionController implements TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); if (pipChange == null) { - return false; + // pipChange is null, check to see if we've reparented the PIP activity for + // the multi activity case. If so we should use the activity leash instead + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + + // failsafe + if (pipChange == null) { + return false; + } + } + + // for multi activity, we need to manually set the leash layer + if (pipChange.getTaskInfo() == null) { + TransitionInfo.Change parent = getChangeByToken(info, pipChange.getParent()); + if (parent != null) { + startTransaction.setLayer(parent.getLeash(), Integer.MAX_VALUE - 1); + } } Rect startBounds = pipChange.getStartAbsBounds(); Rect endBounds = pipChange.getEndAbsBounds(); SurfaceControl pipLeash = pipChange.getLeash(); + Preconditions.checkNotNull(pipLeash, "Leash is null for exit transition."); + + Rect sourceRectHint = null; + if (pipChange.getTaskInfo() != null + && pipChange.getTaskInfo().pictureInPictureParams != null) { + // single activity + sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint(); + } else if (mPipTaskListener.getPictureInPictureParams().hasSourceBoundsHint()) { + // multi activity + sourceRectHint = mPipTaskListener.getPictureInPictureParams().getSourceRectHint(); + } PipEnterExitAnimator animator = new PipEnterExitAnimator(mContext, pipLeash, - startTransaction, startBounds, startBounds, endBounds, - PipEnterExitAnimator.BOUNDS_EXIT); + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + sourceRectHint, PipEnterExitAnimator.BOUNDS_EXIT, Surface.ROTATION_0); + animator.setAnimationEndCallback(() -> { - finishCallback.onTransitionFinished(null); mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + finishCallback.onTransitionFinished(null); }); animator.start(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index b18feefe7eb3..81f444ba2af3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -352,12 +352,17 @@ public class SplashscreenContentDrawer { /** Extract the window background color from {@code attrs}. */ private static int peekWindowBGColor(Context context, SplashScreenWindowAttrs attrs) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "peekWindowBGColor"); - final Drawable themeBGDrawable; + Drawable themeBGDrawable = null; if (attrs.mWindowBgColor != 0) { themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor); } else if (attrs.mWindowBgResId != 0) { - themeBGDrawable = context.getDrawable(attrs.mWindowBgResId); - } else { + try { + themeBGDrawable = context.getDrawable(attrs.mWindowBgResId); + } catch (Resources.NotFoundException e) { + Slog.w(TAG, "Unable get drawable from resource", e); + } + } + if (themeBGDrawable == null) { themeBGDrawable = createDefaultBackgroundDrawable(); Slog.w(TAG, "Window background does not exist, using " + themeBGDrawable); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index a85188a9e04d..82c0aaf3bc8b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -118,6 +118,13 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mTaskViewTaskController.startShortcutActivity(shortcut, options, launchBounds); } + /** + * Moves the current task in taskview out of the view and back to fullscreen. + */ + public void moveToFullscreen() { + mTaskViewTaskController.moveToFullscreen(); + } + @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { if (mTaskViewTaskController.isUsingShellTransitions()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 9750d3ec99f5..0259701a4653 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import android.annotation.NonNull; @@ -256,6 +257,24 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { mTaskViewTransitions.startInstantTransition(TRANSIT_CHANGE, wct); } + /** + * Moves the current task in TaskView out of the view and back to fullscreen. + */ + public void moveToFullscreen() { + if (mTaskToken == null) return; + mShellExecutor.execute(() -> { + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setWindowingMode(mTaskToken, WINDOWING_MODE_UNDEFINED); + wct.setAlwaysOnTop(mTaskToken, false); + mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, false); + mTaskViewTransitions.moveTaskViewToFullscreen(wct, this); + if (mListener != null) { + // Task is being "removed" from the clients perspective + mListener.onTaskRemovalStarted(mTaskInfo.taskId); + } + }); + } + private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) { final Binder launchCookie = new Binder(); mShellExecutor.execute(() -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 15fe7abb96a5..39648f65b4f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -236,6 +236,12 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { startNextTransition(); } + void moveTaskViewToFullscreen(@NonNull WindowContainerTransaction wct, + @NonNull TaskViewTaskController taskView) { + mPending.add(new PendingTransition(TRANSIT_CHANGE, wct, taskView, null /* cookie */)); + startNextTransition(); + } + /** Starts a new transition to make the given {@code taskView} visible. */ public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) { setTaskViewVisible(taskView, visible, false /* reorder */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index ac35459347c6..c88c1e28b011 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -491,7 +491,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else { mInteractionJankMonitor.begin(decoration.mTaskSurface, mContext, Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE, "maximize_menu_resizable"); - mDesktopTasksController.snapToHalfScreen(decoration.mTaskInfo, + mDesktopTasksController.snapToHalfScreen( + decoration.mTaskInfo, + decoration.mTaskSurface, decoration.mTaskInfo.configuration.windowConfiguration.getBounds(), left ? SnapPosition.LEFT : SnapPosition.RIGHT); } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index 507ad647a788..880e02140db1 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -153,6 +153,22 @@ class DesktopModeFlickerScenarios { assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS ) + val EDGE_RESIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("EDGE_RESIZE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + val CORNER_RESIZE_TO_MINIMUM_SIZE = FlickerConfigEntry( scenarioId = ScenarioId("CORNER_RESIZE_TO_MINIMUM_SIZE"), diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeMouse.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeMouse.kt new file mode 100644 index 000000000000..c3abf238dc0d --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeMouse.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.EDGE_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithEdgeResizeMouse : ResizeAppWithEdgeResize(InputMethod.MOUSE) { + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeRight() = super.resizeAppWithEdgeResizeRight() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeLeft() = super.resizeAppWithEdgeResizeLeft() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeTop() = super.resizeAppWithEdgeResizeTop() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeBottom() = super.resizeAppWithEdgeResizeBottom() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(EDGE_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeStylus.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeStylus.kt new file mode 100644 index 000000000000..86b0e6f17b24 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeStylus.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.EDGE_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithEdgeResizeStylus : ResizeAppWithEdgeResize(InputMethod.STYLUS) { + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeRight() = super.resizeAppWithEdgeResizeRight() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeLeft() = super.resizeAppWithEdgeResizeLeft() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeTop() = super.resizeAppWithEdgeResizeTop() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeBottom() = super.resizeAppWithEdgeResizeBottom() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(EDGE_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeTouchpad.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeTouchpad.kt new file mode 100644 index 000000000000..e6bb9eff6715 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppWithEdgeResizeTouchpad.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.EDGE_RESIZE +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppWithEdgeResizeTouchpad : ResizeAppWithEdgeResize(InputMethod.TOUCHPAD) { + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeRight() = super.resizeAppWithEdgeResizeRight() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeLeft() = super.resizeAppWithEdgeResizeLeft() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeTop() = super.resizeAppWithEdgeResizeTop() + + @ExpectedScenarios(["EDGE_RESIZE"]) + @Test + override fun resizeAppWithEdgeResizeBottom() = super.resizeAppWithEdgeResizeBottom() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(EDGE_RESIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt index b812c596adba..426f40b5e81b 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt @@ -16,10 +16,11 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit import android.tools.NavBar import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -36,11 +37,12 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.BlockJUnit4ClassRunner + @RunWith(BlockJUnit4ClassRunner::class) @Postsubmit open class MaximizeAppWindow @JvmOverloads -constructor(rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) { +constructor(private val rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() @@ -57,6 +59,9 @@ constructor(rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = tru @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + ChangeDisplayOrientationRule.setRotation(rotation) testApp.enterDesktopWithDrag(wmHelper, device) } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt index 03d970fe4f39..42940a99b59f 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt @@ -43,8 +43,8 @@ open class ResizeAppWithCornerResize @JvmOverloads constructor( val rotation: Rotation = Rotation.ROTATION_0, - val horizontalChange: Int = 50, - val verticalChange: Int = -50, + val horizontalChange: Int = 200, + val verticalChange: Int = -200, val appProperty: AppProperty = AppProperty.STANDARD ) { diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt new file mode 100644 index 000000000000..d094967e91e0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 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.scenarios + +import android.platform.test.annotations.Postsubmit +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.MotionEventHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class ResizeAppWithEdgeResize +@JvmOverloads +constructor( + val inputMethod: MotionEventHelper.InputMethod, + val rotation: Rotation = Rotation.ROTATION_90 +) { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val motionEventHelper = MotionEventHelper(instrumentation, inputMethod) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue( + Flags.enableDesktopWindowingMode() + && Flags.enableWindowingEdgeDragResize() && tapl.isTablet + ) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun resizeAppWithEdgeResizeRight() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.RIGHT + ) + } + + @Test + open fun resizeAppWithEdgeResizeLeft() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.LEFT + ) + } + + @Test + open fun resizeAppWithEdgeResizeTop() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.TOP + ) + } + + @Test + open fun resizeAppWithEdgeResizeBottom() { + testApp.edgeResize( + wmHelper, + motionEventHelper, + DesktopModeAppHelper.Edges.BOTTOM + ) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt index 685a3ba935d6..33242db66f9f 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt @@ -18,6 +18,8 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -26,9 +28,11 @@ import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags +import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.BlockJUnit4ClassRunner @@ -37,7 +41,7 @@ import org.junit.runners.BlockJUnit4ClassRunner @Postsubmit open class SnapResizeAppWindowWithButton @JvmOverloads -constructor(private val toLeft: Boolean = true, private val isResizable: Boolean = true) { +constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() @@ -49,6 +53,10 @@ constructor(private val toLeft: Boolean = true, private val isResizable: Boolean DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) } + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt index 8a4aa6343e4d..14eb779165bb 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt @@ -18,6 +18,8 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -26,9 +28,11 @@ import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags +import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.BlockJUnit4ClassRunner @@ -37,7 +41,7 @@ import org.junit.runners.BlockJUnit4ClassRunner @Postsubmit open class SnapResizeAppWindowWithDrag @JvmOverloads -constructor(private val toLeft: Boolean = true, private val isResizable: Boolean = true) { +constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() @@ -49,6 +53,10 @@ constructor(private val toLeft: Boolean = true, private val isResizable: Boolean DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) } + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) diff --git a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/Utils.kt b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/Utils.kt index dee67f3f2e0e..c0fafef96775 100644 --- a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/Utils.kt +++ b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/Utils.kt @@ -17,6 +17,7 @@ package com.android.wm.shell import android.app.Instrumentation +import android.platform.test.rule.EnsureDeviceSettingsRule import android.platform.test.rule.NavigationModeRule import android.platform.test.rule.PressHomeRule import android.platform.test.rule.UnlockScreenRule @@ -49,5 +50,6 @@ object Utils { ) ) .around(PressHomeRule()) + .around(EnsureDeviceSettingsRule()) } } diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 4abaf5bd4a38..7305f4988aa9 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -68,6 +68,7 @@ java_defaults { "flickerlib-helpers", "flickerlib-trace_processor_shell", "platform-test-annotations", + "platform-test-rules", "wm-flicker-common-app-helpers", "wm-flicker-common-assertions", "launcher-helper-lib", diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/OWNERS b/libs/WindowManager/Shell/tests/flicker/appcompat/OWNERS new file mode 100644 index 000000000000..a36a4f85fa4e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/OWNERS @@ -0,0 +1,2 @@ +# Window Manager > App Compat +# Bug component: 970984
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt index b85d7936efc2..a9ed13a099f3 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt @@ -16,10 +16,14 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest +import android.tools.flicker.subject.exceptions.ExceptionMessageBuilder +import android.tools.flicker.subject.exceptions.IncorrectRegionException +import android.tools.flicker.subject.layers.LayerSubject import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.pip.common.EnterPipTransition @@ -29,6 +33,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized +import kotlin.math.abs /** * Test entering pip from an app via auto-enter property when navigating to home. @@ -67,9 +72,24 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran override val defaultTeardown: FlickerBuilder.() -> Unit = { teardown { pipApp.exit(wmHelper) } } - @FlakyTest(bugId = 293133362) + private val widthNotSmallerThan: LayerSubject.(LayerSubject) -> Unit = { + val width = visibleRegion.region.bounds.width() + val otherWidth = it.visibleRegion.region.bounds.width() + if (width < otherWidth && abs(width - otherWidth) > EPSILON) { + val errorMsgBuilder = + ExceptionMessageBuilder() + .forSubject(this) + .forIncorrectRegion("width. $width smaller than $otherWidth") + .setExpected(width) + .setActual(otherWidth) + throw IncorrectRegionException(errorMsgBuilder) + } + } + + @Postsubmit @Test override fun pipLayerReduces() { + Assume.assumeFalse(flicker.scenario.isGesturalNavigation) flicker.assertLayers { val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } pipLayerList.zipWithNext { previous, current -> @@ -78,6 +98,32 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran } } + /** Checks that [pipApp] window's width is first decreasing then increasing. */ + @Postsubmit + @Test + fun pipLayerWidthDecreasesThenIncreases() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + flicker.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + var previousLayer = pipLayerList[0] + var currentLayer = previousLayer + var i = 0 + invoke("layer area is decreasing") { + if (i < pipLayerList.size - 1) { + previousLayer = currentLayer + currentLayer = pipLayerList[++i] + previousLayer.widthNotSmallerThan(currentLayer) + } + }.then().invoke("layer are is increasing", true /* isOptional */) { + if (i < pipLayerList.size - 1) { + previousLayer = currentLayer + currentLayer = pipLayerList[++i] + currentLayer.widthNotSmallerThan(previousLayer) + } + } + } + } + /** Checks that [pipApp] window is animated towards default position in right bottom corner */ @FlakyTest(bugId = 255578530) @Test @@ -108,4 +154,9 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran override fun visibleLayersShownMoreThanOneConsecutiveEntry() { super.visibleLayersShownMoreThanOneConsecutiveEntry() } + + companion object { + // TODO(b/363080056): A margin of error allowed on certain layer size calculations. + const val EPSILON = 1 + } } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt index 70be58f06548..429774f890a5 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipAspectRatioChangeTest.kt @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) class PipAspectRatioChangeTest(flicker: LegacyFlickerTest) : PipTransition(flicker) { override val thisTransition: FlickerBuilder.() -> Unit = { - transitions { pipApp.changeAspectRatio() } + transitions { pipApp.changeAspectRatio(wmHelper) } } @Presubmit diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java index 499870220190..f31722d3c1a5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java @@ -19,6 +19,7 @@ package com.android.wm.shell; import com.android.wm.shell.common.ShellExecutor; import java.util.ArrayList; +import java.util.List; /** * Really basic test executor. It just gathers all events in a blob. The only option is to @@ -52,4 +53,9 @@ public class TestShellExecutor implements ShellExecutor { mRunnables.remove(0).run(); } } + + /** Returns the list of callbacks for this executor. */ + public List<Runnable> getCallbacks() { + return mRunnables; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 859602ec709f..6fa37885b724 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -50,8 +50,8 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.bubbles.BubbleData.TimeSource; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.google.common.collect.ImmutableList; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index 50c4a1828026..dca5fc4c2fe0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -43,7 +43,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.bubbles.BubbleInfo; +import com.android.wm.shell.shared.bubbles.BubbleInfo; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 5474e539f286..10557dd9b439 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -123,12 +123,12 @@ import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.verify +import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.eq @@ -2859,7 +2859,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun getSnapBounds_calculatesBoundsForResizable() { + fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() { val bounds = Rect(100, 100, 300, 300) val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { topActivityInfo = ActivityInfo().apply { @@ -2874,13 +2874,45 @@ class DesktopTasksControllerTest : ShellTestCase() { STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom ) - controller.snapToHalfScreen(task, currentDragBounds, SnapPosition.LEFT) + controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT) // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) } @Test + fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + // Set up task to already be in snapped-left bounds + val bounds = Rect( + STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom + ) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { + topActivityInfo = ActivityInfo().apply { + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } + isResizeable = true + } + + // Attempt to snap left again + val currentDragBounds = Rect(bounds).apply { offset(-100, 0) } + controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT) + + // Assert that task is NOT updated via WCT + verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + + // Assert that task leash is updated via Surface Animations + verify(mReturnToDragStartAnimator).start( + eq(task.taskId), + eq(mockSurface), + eq(currentDragBounds), + eq(bounds), + eq(true) + ) + } + + @Test @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) fun handleSnapResizingTask_nonResizable_snapsToHalfScreen() { val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { @@ -2911,7 +2943,8 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(task.taskId), eq(mockSurface), eq(currentDragBounds), - eq(preDragBounds) + eq(preDragBounds), + eq(false) ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleBarLocationTest.kt index 27e0b196f0be..b9bf95b16e70 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleBarLocationTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.common.bubbles.BubbleBarLocation.DEFAULT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT -import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.DEFAULT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT +import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleInfoTest.kt index 5b22eddcb6ee..641063c27076 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/BubbleInfoTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.common.bubbles +package com.android.wm.shell.shared.bubbles import android.os.Parcel import android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE @@ -41,6 +41,7 @@ class BubbleInfoTest : ShellTestCase() { "com.some.package", "title", "Some app", + true, true ) val parcel = Parcel.obtain() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/handles/RegionSamplingHelperTest.kt index 2a6754cbdd1a..d3e291f7dd1f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/navigationbar/RegionSamplingHelperTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/handles/RegionSamplingHelperTest.kt @@ -14,7 +14,8 @@ * limitations under the License. */ -package com.android.systemui.shared.navigationbar +package com.android.wm.shell.shared.handles + import android.graphics.Rect import android.testing.TestableLooper.RunWithLooper @@ -24,16 +25,15 @@ import android.view.ViewRootImpl import androidx.concurrent.futures.DirectExecutor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor -import com.android.systemui.util.time.FakeSystemClock +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock @@ -42,11 +42,12 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.argumentCaptor @RunWith(AndroidJUnit4::class) @SmallTest @RunWithLooper -class RegionSamplingHelperTest : SysuiTestCase() { +class RegionSamplingHelperTest : ShellTestCase() { @Mock lateinit var sampledView: View @@ -72,12 +73,16 @@ class RegionSamplingHelperTest : SysuiTestCase() { whenever(surfaceControl.isValid).thenReturn(true) whenever(wrappedSurfaceControl.isValid).thenReturn(true) whenever(samplingCallback.isSamplingEnabled).thenReturn(true) - regionSamplingHelper = object : RegionSamplingHelper(sampledView, samplingCallback, - DirectExecutor.INSTANCE, DirectExecutor.INSTANCE, compositionListener) { - override fun wrap(stopLayerControl: SurfaceControl?): SurfaceControl { - return wrappedSurfaceControl + getInstrumentation().runOnMainSync(Runnable { + regionSamplingHelper = object : RegionSamplingHelper( + sampledView, samplingCallback, + DirectExecutor.INSTANCE, DirectExecutor.INSTANCE, compositionListener + ) { + override fun wrap(stopLayerControl: SurfaceControl?): SurfaceControl { + return wrappedSurfaceControl + } } - } + }) regionSamplingHelper.setWindowVisible(true) } @@ -99,7 +104,7 @@ class RegionSamplingHelperTest : SysuiTestCase() { regionSamplingHelper.setWindowHasBlurs(true) regionSamplingHelper.start(Rect(0, 0, 100, 100)) verify(compositionListener, never()) - .register(any(), anyInt(), eq(wrappedSurfaceControl), any()) + .register(any(), anyInt(), eq(wrappedSurfaceControl), any()) } @Test @@ -112,35 +117,38 @@ class RegionSamplingHelperTest : SysuiTestCase() { @Test fun testCompositionSamplingListener_has_nonEmptyRect() { // simulate race condition - val fakeExecutor = FakeExecutor(FakeSystemClock()) // pass in as backgroundExecutor + val fakeExecutor = TestShellExecutor() // pass in as backgroundExecutor val fakeSamplingCallback = mock(RegionSamplingHelper.SamplingCallback::class.java) whenever(fakeSamplingCallback.isSamplingEnabled).thenReturn(true) whenever(wrappedSurfaceControl.isValid).thenReturn(true) - - regionSamplingHelper = object : RegionSamplingHelper(sampledView, fakeSamplingCallback, - DirectExecutor.INSTANCE, fakeExecutor, compositionListener) { - override fun wrap(stopLayerControl: SurfaceControl?): SurfaceControl { - return wrappedSurfaceControl + getInstrumentation().runOnMainSync(Runnable { + regionSamplingHelper = object : RegionSamplingHelper( + sampledView, fakeSamplingCallback, + DirectExecutor.INSTANCE, fakeExecutor, compositionListener + ) { + override fun wrap(stopLayerControl: SurfaceControl?): SurfaceControl { + return wrappedSurfaceControl + } } - } + }) regionSamplingHelper.setWindowVisible(true) regionSamplingHelper.start(Rect(0, 0, 100, 100)) // make sure background task is enqueued - assertThat(fakeExecutor.numPending()).isEqualTo(1) + assertThat(fakeExecutor.getCallbacks().size).isEqualTo(1) // make sure regionSamplingHelper will have empty Rect whenever(fakeSamplingCallback.getSampledRegion(any())).thenReturn(Rect(0, 0, 0, 0)) regionSamplingHelper.onLayoutChange(sampledView, 0, 0, 0, 0, 0, 0, 0, 0) // resume running of background thread - fakeExecutor.runAllReady() + fakeExecutor.flushAll() // grab Rect passed into compositionSamplingListener and make sure it's not empty val argumentGrabber = argumentCaptor<Rect>() verify(compositionListener).register(any(), anyInt(), eq(wrappedSurfaceControl), - argumentGrabber.capture()) - assertThat(argumentGrabber.value.isEmpty).isFalse() + argumentGrabber.capture()) + assertThat(argumentGrabber.firstValue.isEmpty).isFalse() } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 0434742c571b..c596ca3fca6b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -713,4 +713,16 @@ public class TaskViewTest extends ShellTestCase { verify(mViewHandler).post(any()); verify(mTaskView).setResizeBackgroundColor(eq(Color.BLUE)); } + + @Test + public void testMoveToFullscreen_callsTaskRemovalStarted() { + WindowContainerTransaction wct = new WindowContainerTransaction(); + mTaskViewTaskController.prepareOpenAnimation(true /* newTask */, + new SurfaceControl.Transaction(), new SurfaceControl.Transaction(), mTaskInfo, + mLeash, wct); + mTaskView.surfaceCreated(mock(SurfaceHolder.class)); + mTaskViewTaskController.moveToFullscreen(); + + verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId)); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 0b5c6784b73d..be0549b6655d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -113,7 +113,7 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times -import org.mockito.Mockito.verify +import org.mockito.kotlin.verify import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat @@ -600,6 +600,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Test fun testOnDecorSnappedLeft_snapResizes() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -610,8 +611,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onLeftSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController) - .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.LEFT) + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.LEFT) + ) + assertEquals(taskSurfaceCaptor.firstValue, decor.mTaskSurface) } @Test @@ -632,6 +638,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) fun testOnSnapResizeLeft_nonResizable_decorSnappedLeft() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onLeftSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -642,8 +649,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onLeftSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController) - .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.LEFT) + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.LEFT) + ) + assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @Test @@ -660,12 +672,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onLeftSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.LEFT) + .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT)) verify(mockToast).show() } @Test fun testOnDecorSnappedRight_snapResizes() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onRightSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -676,8 +689,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onRightSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController) - .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.RIGHT) + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.RIGHT) + ) + assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @Test @@ -698,6 +716,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) fun testOnSnapResizeRight_nonResizable_decorSnappedRight() { + val taskSurfaceCaptor = argumentCaptor<SurfaceControl>() val onRightSnapClickListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -708,8 +727,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { val currentBounds = decor.mTaskInfo.configuration.windowConfiguration.bounds onRightSnapClickListenerCaptor.value.invoke() - verify(mockDesktopTasksController) - .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.RIGHT) + verify(mockDesktopTasksController).snapToHalfScreen( + eq(decor.mTaskInfo), + taskSurfaceCaptor.capture(), + eq(currentBounds), + eq(SnapPosition.RIGHT) + ) + assertEquals(decor.mTaskSurface, taskSurfaceCaptor.firstValue) } @Test @@ -726,7 +750,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { onRightSnapClickListenerCaptor.value.invoke() verify(mockDesktopTasksController, never()) - .snapToHalfScreen(decor.mTaskInfo, currentBounds, SnapPosition.RIGHT) + .snapToHalfScreen(eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT)) verify(mockToast).show() } @@ -1033,6 +1057,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, + taskSurface: SurfaceControl = SurfaceControl(), onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>, onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> = @@ -1051,7 +1076,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { forClass(View.OnClickListener::class.java) as ArgumentCaptor<View.OnClickListener> ): DesktopModeWindowDecoration { val decor = setUpMockDecorationForTask(createTask(windowingMode = windowingMode)) - onTaskOpening(decor.mTaskInfo) + onTaskOpening(decor.mTaskInfo, taskSurface) verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture()) verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture()) verify(decor).setOnRightSnapClickListener(onRightSnapClickListenerCaptor.capture()) diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp index eecc741a3bbb..1afef75bc741 100644 --- a/libs/input/MouseCursorController.cpp +++ b/libs/input/MouseCursorController.cpp @@ -25,6 +25,9 @@ #include <input/Input.h> #include <log/log.h> +#define INDENT " " +#define INDENT2 " " + namespace { // Time to spend fading out the pointer completely. const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms @@ -449,6 +452,24 @@ bool MouseCursorController::resourcesLoaded() { return mLocked.resourcesLoaded; } +std::string MouseCursorController::dump() const { + std::string dump = INDENT "MouseCursorController:\n"; + std::scoped_lock lock(mLock); + dump += StringPrintf(INDENT2 "viewport: %s\n", mLocked.viewport.toString().c_str()); + dump += StringPrintf(INDENT2 "stylusHoverMode: %s\n", + mLocked.stylusHoverMode ? "true" : "false"); + dump += StringPrintf(INDENT2 "pointerFadeDirection: %d\n", mLocked.pointerFadeDirection); + dump += StringPrintf(INDENT2 "updatePointerIcon: %s\n", + mLocked.updatePointerIcon ? "true" : "false"); + dump += StringPrintf(INDENT2 "resourcesLoaded: %s\n", + mLocked.resourcesLoaded ? "true" : "false"); + dump += StringPrintf(INDENT2 "requestedPointerType: %d\n", mLocked.requestedPointerType); + dump += StringPrintf(INDENT2 "resolvedPointerType: %d\n", mLocked.resolvedPointerType); + dump += StringPrintf(INDENT2 "skipScreenshot: %s\n", mLocked.skipScreenshot ? "true" : "false"); + dump += StringPrintf(INDENT2 "animating: %s\n", mLocked.animating ? "true" : "false"); + return dump; +} + bool MouseCursorController::doAnimations(nsecs_t timestamp) { std::scoped_lock lock(mLock); bool keepFading = doFadingAnimationLocked(timestamp); diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h index 78f6413ff111..860034141a0b 100644 --- a/libs/input/MouseCursorController.h +++ b/libs/input/MouseCursorController.h @@ -67,6 +67,8 @@ public: bool resourcesLoaded(); + std::string dump() const; + private: mutable std::mutex mLock; diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 11b27a214984..5ae967bc369a 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -25,6 +25,7 @@ #include <android-base/stringprintf.h> #include <android-base/thread_annotations.h> #include <ftl/enum.h> +#include <input/PrintTools.h> #include <mutex> @@ -353,6 +354,8 @@ std::string PointerController::dump() { for (const auto& [_, spotController] : mLocked.spotControllers) { spotController.dump(dump, INDENT3); } + dump += INDENT2 "Cursor Controller:\n"; + dump += addLinePrefix(mCursorController.dump(), INDENT3); return dump; } diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index a255f730b0f3..ebdfd3e41aa1 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2655,7 +2655,16 @@ public class AudioSystem /** * Register a native listener for system property sysprop * @param callback the listener which fires when the property changes + * @return a native handle for use in subsequent methods * @hide */ - public static native void listenForSystemPropertyChange(String sysprop, Runnable callback); + public static native long listenForSystemPropertyChange(String sysprop, Runnable callback); + + /** + * Trigger a sysprop listener update, if the property has been updated: synchronously validating + * there are no pending sysprop changes. + * @param handle the handle returned by {@link listenForSystemPropertyChange} + * @hide + */ + public static native void triggerSystemPropertyUpdate(long handle); } diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index 717e01e18dbd..0f97b2c8d443 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -73,6 +73,7 @@ package android.nfc { method public void onApplyRouting(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onBootFinished(int); method public void onBootStarted(); + method public void onCardEmulationActivated(boolean); method public void onDisable(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onDisableFinished(int); method public void onDisableStarted(); @@ -81,6 +82,8 @@ package android.nfc { method public void onEnableStarted(); method public void onHceEventReceived(int); method public void onNdefRead(@NonNull java.util.function.Consumer<java.lang.Boolean>); + method public void onRfDiscoveryStarted(boolean); + method public void onRfFieldActivated(boolean); method public void onRoutingChanged(); method public void onStateUpdated(int); method public void onTagConnected(boolean, @NonNull android.nfc.Tag); diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl index c19a44ba0ff1..b65c83773618 100644 --- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl +++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl @@ -37,4 +37,7 @@ interface INfcOemExtensionCallback { void onTagDispatch(in ResultReceiver isSkipped); void onRoutingChanged(); void onHceEventReceived(int action); + void onCardEmulationActivated(boolean isActivated); + void onRfFieldActivated(boolean isActivated); + void onRfDiscoveryStarted(boolean isDiscoveryStarted); } diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index 6c02edd0eafa..632f693c4fad 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -32,7 +32,9 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; @@ -40,6 +42,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -58,10 +61,13 @@ public final class NfcOemExtension { private static final int OEM_EXTENSION_RESPONSE_THRESHOLD_MS = 2000; private final NfcAdapter mAdapter; private final NfcOemExtensionCallback mOemNfcExtensionCallback; + private boolean mIsRegistered = false; + private final Map<Callback, Executor> mCallbackMap = new HashMap<>(); private final Context mContext; - private Executor mExecutor = null; - private Callback mCallback = null; private final Object mLock = new Object(); + private boolean mCardEmulationActivated = false; + private boolean mRfFieldActivated = false; + private boolean mRfDiscoveryStarted = false; /** * Event that Host Card Emulation is activated. @@ -215,6 +221,32 @@ public final class NfcOemExtension { * @param action Flag indicating actions to activate, start and stop cpu boost. */ void onHceEventReceived(@HostCardEmulationAction int action); + + /** + * Notifies NFC is activated in listen mode. + * NFC Forum NCI-2.3 ch.5.2.6 specification + * + * <p>NFCC is ready to communicate with a Card reader + * + * @param isActivated true, if card emulation activated, else de-activated. + */ + void onCardEmulationActivated(boolean isActivated); + + /** + * Notifies the Remote NFC Endpoint RF Field is activated. + * NFC Forum NCI-2.3 ch.5.3 specification + * + * @param isActivated true, if RF Field is ON, else RF Field is OFF. + */ + void onRfFieldActivated(boolean isActivated); + + /** + * Notifies the NFC RF discovery is started or in the IDLE state. + * NFC Forum NCI-2.3 ch.5.2 specification + * + * @param isDiscoveryStarted true, if RF discovery started, else RF state is Idle. + */ + void onRfDiscoveryStarted(boolean isDiscoveryStarted); } @@ -229,7 +261,12 @@ public final class NfcOemExtension { /** * Register an {@link Callback} to listen for NFC oem extension callbacks + * Multiple clients can register and callbacks will be invoked asynchronously. + * * <p>The provided callback will be invoked by the given {@link Executor}. + * As part of {@link #registerCallback(Executor, Callback)} the + * {@link Callback} will be invoked with current NFC state + * before the {@link #registerCallback(Executor, Callback)} function completes. * * @param executor an {@link Executor} to execute given callback * @param callback oem implementation of {@link Callback} @@ -239,15 +276,35 @@ public final class NfcOemExtension { public void registerCallback(@NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) { synchronized (mLock) { - if (mCallback != null) { + if (executor == null || callback == null) { + Log.e(TAG, "Executor and Callback must not be null!"); + throw new IllegalArgumentException(); + } + + if (mCallbackMap.containsKey(callback)) { Log.e(TAG, "Callback already registered. Unregister existing callback before" + "registering"); throw new IllegalArgumentException(); } - NfcAdapter.callService(() -> { - NfcAdapter.sService.registerOemExtensionCallback(mOemNfcExtensionCallback); - mCallback = callback; - mExecutor = executor; + mCallbackMap.put(callback, executor); + if (!mIsRegistered) { + NfcAdapter.callService(() -> { + NfcAdapter.sService.registerOemExtensionCallback(mOemNfcExtensionCallback); + mIsRegistered = true; + }); + } else { + updateNfCState(callback, executor); + } + } + } + + private void updateNfCState(Callback callback, Executor executor) { + if (callback != null) { + Log.i(TAG, "updateNfCState"); + executor.execute(() -> { + callback.onCardEmulationActivated(mCardEmulationActivated); + callback.onRfFieldActivated(mRfFieldActivated); + callback.onRfDiscoveryStarted(mRfDiscoveryStarted); }); } } @@ -266,15 +323,19 @@ public final class NfcOemExtension { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterCallback(@NonNull Callback callback) { synchronized (mLock) { - if (mCallback == null || mCallback != callback) { + if (!mCallbackMap.containsKey(callback) || !mIsRegistered) { Log.e(TAG, "Callback not registered"); throw new IllegalArgumentException(); } - NfcAdapter.callService(() -> { - NfcAdapter.sService.unregisterOemExtensionCallback(mOemNfcExtensionCallback); - mCallback = null; - mExecutor = null; - }); + if (mCallbackMap.size() == 1) { + NfcAdapter.callService(() -> { + NfcAdapter.sService.unregisterOemExtensionCallback(mOemNfcExtensionCallback); + mIsRegistered = false; + mCallbackMap.remove(callback); + }); + } else { + mCallbackMap.remove(callback); + } } } @@ -322,90 +383,133 @@ public final class NfcOemExtension { } private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub { + @Override public void onTagConnected(boolean connected, Tag tag) throws RemoteException { - synchronized (mLock) { - if (mCallback == null || mExecutor == null) { - return; - } - final long identity = Binder.clearCallingIdentity(); - try { - mExecutor.execute(() -> mCallback.onTagConnected(connected, tag)); - } finally { - Binder.restoreCallingIdentity(identity); - } - } + mCallbackMap.forEach((cb, ex) -> + handleVoid2ArgCallback(connected, tag, cb::onTagConnected, ex)); + } + + @Override + public void onCardEmulationActivated(boolean isActivated) throws RemoteException { + mCardEmulationActivated = isActivated; + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(isActivated, cb::onCardEmulationActivated, ex)); + } + + @Override + public void onRfFieldActivated(boolean isActivated) throws RemoteException { + mRfFieldActivated = isActivated; + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(isActivated, cb::onRfFieldActivated, ex)); + } + + @Override + public void onRfDiscoveryStarted(boolean isDiscoveryStarted) throws RemoteException { + mRfDiscoveryStarted = isDiscoveryStarted; + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(isDiscoveryStarted, cb::onRfDiscoveryStarted, ex)); } + @Override public void onStateUpdated(int state) throws RemoteException { - handleVoidCallback(state, mCallback::onStateUpdated); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(state, cb::onStateUpdated, ex)); } + @Override public void onApplyRouting(ResultReceiver isSkipped) throws RemoteException { - handleVoidCallback( - new ReceiverWrapper(isSkipped), mCallback::onApplyRouting); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback( + new ReceiverWrapper(isSkipped), cb::onApplyRouting, ex)); } @Override public void onNdefRead(ResultReceiver isSkipped) throws RemoteException { - handleVoidCallback( - new ReceiverWrapper(isSkipped), mCallback::onNdefRead); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback( + new ReceiverWrapper(isSkipped), cb::onNdefRead, ex)); } @Override public void onEnable(ResultReceiver isAllowed) throws RemoteException { - handleVoidCallback( - new ReceiverWrapper(isAllowed), mCallback::onEnable); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback( + new ReceiverWrapper(isAllowed), cb::onEnable, ex)); } @Override public void onDisable(ResultReceiver isAllowed) throws RemoteException { - handleVoidCallback( - new ReceiverWrapper(isAllowed), mCallback::onDisable); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback( + new ReceiverWrapper(isAllowed), cb::onDisable, ex)); } @Override public void onBootStarted() throws RemoteException { - handleVoidCallback(null, (Object input) -> mCallback.onBootStarted()); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(null, (Object input) -> cb.onBootStarted(), ex)); } @Override public void onEnableStarted() throws RemoteException { - handleVoidCallback(null, (Object input) -> mCallback.onEnableStarted()); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(null, (Object input) -> cb.onEnableStarted(), ex)); } @Override public void onDisableStarted() throws RemoteException { - handleVoidCallback(null, (Object input) -> mCallback.onDisableStarted()); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(null, (Object input) -> cb.onDisableStarted(), ex)); } @Override public void onBootFinished(int status) throws RemoteException { - handleVoidCallback(status, mCallback::onBootFinished); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(status, cb::onBootFinished, ex)); } @Override public void onEnableFinished(int status) throws RemoteException { - handleVoidCallback(status, mCallback::onEnableFinished); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(status, cb::onEnableFinished, ex)); } @Override public void onDisableFinished(int status) throws RemoteException { - handleVoidCallback(status, mCallback::onDisableFinished); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(status, cb::onDisableFinished, ex)); } @Override public void onTagDispatch(ResultReceiver isSkipped) throws RemoteException { - handleVoidCallback( - new ReceiverWrapper(isSkipped), mCallback::onTagDispatch); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback( + new ReceiverWrapper(isSkipped), cb::onTagDispatch, ex)); } @Override public void onRoutingChanged() throws RemoteException { - handleVoidCallback(null, (Object input) -> mCallback.onRoutingChanged()); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(null, (Object input) -> cb.onRoutingChanged(), ex)); } @Override public void onHceEventReceived(int action) throws RemoteException { - handleVoidCallback(action, mCallback::onHceEventReceived); + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(action, cb::onHceEventReceived, ex)); } - private <T> void handleVoidCallback(T input, Consumer<T> callbackMethod) { + private <T> void handleVoidCallback( + T input, Consumer<T> callbackMethod, Executor executor) { synchronized (mLock) { - if (mCallback == null || mExecutor == null) { - return; + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> callbackMethod.accept(input)); + } catch (RuntimeException ex) { + throw ex; + } finally { + Binder.restoreCallingIdentity(identity); } + } + } + + private <T1, T2> void handleVoid2ArgCallback( + T1 input1, T2 input2, BiConsumer<T1, T2> callbackMethod, Executor executor) { + synchronized (mLock) { final long identity = Binder.clearCallingIdentity(); try { - mExecutor.execute(() -> callbackMethod.accept(input)); + executor.execute(() -> callbackMethod.accept(input1, input2)); + } catch (RuntimeException ex) { + throw ex; } finally { Binder.restoreCallingIdentity(identity); } @@ -415,17 +519,12 @@ public final class NfcOemExtension { private <S, T> S handleNonVoidCallbackWithInput( S defaultValue, T input, Function<T, S> callbackMethod) throws RemoteException { synchronized (mLock) { - if (mCallback == null) { - return defaultValue; - } final long identity = Binder.clearCallingIdentity(); S result = defaultValue; try { ExecutorService executor = Executors.newSingleThreadExecutor(); - FutureTask<S> futureTask = new FutureTask<>( - () -> callbackMethod.apply(input) - ); - executor.submit(futureTask); + FutureTask<S> futureTask = new FutureTask<>(() -> callbackMethod.apply(input)); + var unused = executor.submit(futureTask); try { result = futureTask.get( OEM_EXTENSION_RESPONSE_THRESHOLD_MS, TimeUnit.MILLISECONDS); @@ -447,17 +546,12 @@ public final class NfcOemExtension { private <T> T handleNonVoidCallbackWithoutInput(T defaultValue, Supplier<T> callbackMethod) throws RemoteException { synchronized (mLock) { - if (mCallback == null) { - return defaultValue; - } final long identity = Binder.clearCallingIdentity(); T result = defaultValue; try { ExecutorService executor = Executors.newSingleThreadExecutor(); - FutureTask<T> futureTask = new FutureTask<>( - callbackMethod::get - ); - executor.submit(futureTask); + FutureTask<T> futureTask = new FutureTask<>(callbackMethod::get); + var unused = executor.submit(futureTask); try { result = futureTask.get( OEM_EXTENSION_RESPONSE_THRESHOLD_MS, TimeUnit.MILLISECONDS); diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index 0fda91d2b48e..0ade4db47760 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -141,3 +141,11 @@ flag { description: "Enable override and recover routing table" bug: "329043523" } + +flag { + name: "nfc_watchdog" + is_exported: true + namespace: "nfc" + description: "Enable watchdog for the NFC system process" + bug: "362937338" +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index a543450821b8..3011ce05c3a5 100644 --- a/packages/SettingsLib/Spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/build.gradle.kts @@ -29,7 +29,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath allprojects { extra["androidTop"] = androidTop - extra["jetpackComposeVersion"] = "1.7.0-beta07" + extra["jetpackComposeVersion"] = "1.7.0-rc01" } subprojects { diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml index 3507605c5ad2..d01c0b90481c 100644 --- a/packages/SettingsLib/Spa/gradle/libs.versions.toml +++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml @@ -15,7 +15,7 @@ # [versions] -agp = "8.5.2" +agp = "8.6.0" compose-compiler = "1.5.11" dexmaker-mockito = "2.28.3" jvm = "17" diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.9-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip Binary files differindex 9a97e4674448..50432f3369c6 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.9-bin.zip +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar Binary files differindex 2c3521197d7c..a4b76b9530d6 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties index 9f29c77d55f6..9a7f4b60b773 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties @@ -16,6 +16,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=gradle-8.9-bin.zip +distributionUrl=gradle-8.10-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index e9153e3e6010..f0c2ea6f5353 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -54,13 +54,13 @@ android { dependencies { api(project(":SettingsLibColor")) api("androidx.appcompat:appcompat:1.7.0") - api("androidx.compose.material3:material3:1.3.0-beta05") + api("androidx.compose.material3:material3:1.3.0-rc01") api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion") api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion") api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion") api("androidx.lifecycle:lifecycle-livedata-ktx") api("androidx.lifecycle:lifecycle-runtime-compose") - api("androidx.navigation:navigation-compose:2.8.0-beta07") + api("androidx.navigation:navigation-compose:2.8.0-rc01") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") api("com.google.android.material:material:1.11.0") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt index aceb5458797d..62af08e8cfdc 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt @@ -117,7 +117,7 @@ internal fun InternalSwitchPreference( val indication = LocalIndication.current val onChangeWithLog = wrapOnSwitchWithLog(onCheckedChange) val interactionSource = remember { MutableInteractionSource() } - val modifier = remember(checked, changeable) { + val modifier = if (checked != null && onChangeWithLog != null) { Modifier.toggleable( value = checked, @@ -128,7 +128,6 @@ internal fun InternalSwitchPreference( onValueChange = onChangeWithLog, ) } else Modifier - } BasePreference( title = title, summary = summary, diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeAlignment.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl index 0690537944e2..1726036f0ded 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/shared/model/ShadeAlignment.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl @@ -14,14 +14,6 @@ * limitations under the License. */ -package com.android.systemui.shade.shared.model +package com.android.settingslib.bluetooth.devicesettings; -/** Enumerates all supported alignments of the shade. */ -sealed interface ShadeAlignment { - - /** Aligns the shade to the top. */ - data object Top : ShadeAlignment - - /** Aligns the shade to the bottom. */ - data object Bottom : ShadeAlignment -} +parcelable DeviceSettingsProviderServiceStatus;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt new file mode 100644 index 000000000000..977849e75556 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable + +/** + * A data class representing a device settings item in bluetooth device details config. + * + * @property enabled Whether the service is enabled. + * @property extras Extra bundle + */ +data class DeviceSettingsProviderServiceStatus( + val enabled: Boolean, + val extras: Bundle = Bundle.EMPTY, +) : Parcelable { + + override fun describeContents(): Int = 0 + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.run { + writeBoolean(enabled) + writeBundle(extras) + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<DeviceSettingsProviderServiceStatus> = + object : Parcelable.Creator<DeviceSettingsProviderServiceStatus> { + override fun createFromParcel(parcel: Parcel) = + parcel.run { + DeviceSettingsProviderServiceStatus( + enabled = readBoolean(), + extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, + ) + } + + override fun newArray(size: Int): Array<DeviceSettingsProviderServiceStatus?> { + return arrayOfNulls(size) + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl index d5efac9d0336..1c0a1fd6b798 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl @@ -18,10 +18,12 @@ package com.android.settingslib.bluetooth.devicesettings; import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState; +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus; import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener; -oneway interface IDeviceSettingsProviderService { - void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); - void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); - void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); +interface IDeviceSettingsProviderService { + DeviceSettingsProviderServiceStatus getServiceStatus(); + oneway void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); + oneway void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); + oneway void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); }
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt new file mode 100644 index 000000000000..25080bcef061 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/model/ServiceConnectionStatus.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings.data.model + +import android.os.IInterface + +/** Present a service connection status. */ +sealed interface ServiceConnectionStatus<out T : IInterface> { + /** Service is connecting. */ + data object Connecting : ServiceConnectionStatus<Nothing> + + /** Service is connected. */ + data class Connected<T : IInterface>(val service: T) : ServiceConnectionStatus<T> + + /** Service connection failed. */ + data object Failed : ServiceConnectionStatus<Nothing> +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index d6b28629d16b..33beb06e2ed5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -22,7 +22,8 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder -import com.android.internal.util.ConcurrentUtils +import android.os.IInterface +import android.util.Log import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceInfo @@ -34,27 +35,28 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService +import com.android.settingslib.bluetooth.devicesettings.data.model.ServiceConnectionStatus import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -84,64 +86,132 @@ class DeviceSettingServiceConnection( } } - private var config = AtomicReference<DeviceSettingsConfig?>(null) - private var idToSetting = AtomicReference<Flow<Map<Int, DeviceSetting>>?>(null) + private var isServiceEnabled = + coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { + val states = getSettingsProviderServices()?.values ?: return@async false + combine(states) { it.toList() } + .mapNotNull { allStatus -> + if (allStatus.any { it is ServiceConnectionStatus.Failed }) { + false + } else if (allStatus.all { it is ServiceConnectionStatus.Connected }) { + allStatus + .filterIsInstance< + ServiceConnectionStatus.Connected<IDeviceSettingsProviderService> + >() + .all { it.service.serviceStatus?.enabled == true } + } else { + null + } + } + .first() + } - /** Gets [DeviceSettingsConfig] for the device, return null when failed. */ - suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? = - config.computeIfAbsent { - getConfigServiceBindingIntent(cachedDevice) - .flatMapLatest { getService(it) } - .map { it?.let { IDeviceSettingsConfigProviderService.Stub.asInterface(it) } } - .map { - it?.getDeviceSettingsConfig( - deviceInfo { setBluetoothAddress(cachedDevice.address) } - ) + private var config = + coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) { + val intent = + tryGetEndpointFromMetadata(cachedDevice)?.toIntent() + ?: run { + Log.i(TAG, "Unable to read device setting metadata from $cachedDevice") + return@async null + } + getService(intent, IDeviceSettingsConfigProviderService.Stub::asInterface) + .flatMapConcat { + when (it) { + is ServiceConnectionStatus.Connected -> + flowOf( + it.service.getDeviceSettingsConfig( + deviceInfo { setBluetoothAddress(cachedDevice.address) } + ) + ) + ServiceConnectionStatus.Connecting -> flowOf() + ServiceConnectionStatus.Failed -> flowOf(null) + } } .first() } + private val settingIdToItemMapping = + flow { + if (!isServiceEnabled.await()) { + Log.w(TAG, "Service is disabled") + return@flow + } + getSettingsProviderServices() + ?.values + ?.map { + it.flatMapLatest { status -> + when (status) { + is ServiceConnectionStatus.Connected -> + getDeviceSettingsFromService(cachedDevice, status.service) + else -> flowOf(emptyList()) + } + } + } + ?.let { items -> combine(items) { it.toList().flatten() } } + ?.map { items -> items.associateBy { it.settingId } } + ?.let { emitAll(it) } + } + .shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1) + + /** Gets [DeviceSettingsConfig] for the device, return null when failed. */ + suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? { + if (!isServiceEnabled.await()) { + Log.w(TAG, "Service is disabled") + return null + } + return readConfig() + } + /** Gets all device settings for the device. */ fun getDeviceSettingList(): Flow<List<DeviceSetting>> = - getSettingIdToItemMapping().map { it.values.toList() } + settingIdToItemMapping.map { it.values.toList() } /** Gets the device settings with the ID for the device. */ fun getDeviceSetting(@DeviceSettingId deviceSettingId: Int): Flow<DeviceSetting?> = - getSettingIdToItemMapping().map { it[deviceSettingId] } + settingIdToItemMapping.map { it[deviceSettingId] } /** Updates the device setting state for the device. */ suspend fun updateDeviceSettings( @DeviceSettingId deviceSettingId: Int, deviceSettingPreferenceState: DeviceSettingPreferenceState, ) { - getDeviceSettingsConfig()?.let { config -> + if (!isServiceEnabled.await()) { + Log.w(TAG, "Service is disabled") + return + } + readConfig()?.let { config -> (config.mainContentItems + config.moreSettingsItems) .find { it.settingId == deviceSettingId } ?.let { getSettingsProviderServices() ?.get(EndPoint(it.packageName, it.className, it.intentAction)) - ?.filterNotNull() + ?.filterIsInstance< + ServiceConnectionStatus.Connected<IDeviceSettingsProviderService> + >() ?.first() } + ?.service ?.updateDeviceSettings( deviceInfo { setBluetoothAddress(cachedDevice.address) }, DeviceSettingState.Builder() .setSettingId(deviceSettingId) .setPreferenceState(deviceSettingPreferenceState) - .build() + .build(), ) } } + private suspend fun readConfig(): DeviceSettingsConfig? = config.await() + private suspend fun getSettingsProviderServices(): - Map<EndPoint, StateFlow<IDeviceSettingsProviderService?>>? = - getDeviceSettingsConfig() + Map<EndPoint, StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>>? = + readConfig() ?.let { config -> (config.mainContentItems + config.moreSettingsItems).map { EndPoint( packageName = it.packageName, className = it.className, - intentAction = it.intentAction + intentAction = it.intentAction, ) } } @@ -150,43 +220,22 @@ class DeviceSettingServiceConnection( { it }, { endpoint -> services.computeIfAbsent(endpoint) { - getService(endpoint.toIntent()) - .map { service -> - IDeviceSettingsProviderService.Stub.asInterface(service) - } - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) + getService( + endpoint.toIntent(), + IDeviceSettingsProviderService.Stub::asInterface, + ) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + ServiceConnectionStatus.Connecting, + ) } - } + }, ) - private fun getSettingIdToItemMapping(): Flow<Map<Int, DeviceSetting>> = - idToSetting.computeIfAbsent { - flow { - getSettingsProviderServices() - ?.values - ?.map { - it.flatMapLatest { service -> - if (service != null) { - getDeviceSettingsFromService(cachedDevice, service) - } else { - flowOf(emptyList()) - } - } - } - ?.let { items -> combine(items) { it.toList().flatten() } } - ?.map { items -> items.associateBy { it.settingId } } - ?.let { emitAll(it) } - } - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1 - ) - }!! - private fun getDeviceSettingsFromService( cachedDevice: CachedBluetoothDevice, - service: IDeviceSettingsProviderService + service: IDeviceSettingsProviderService, ): Flow<List<DeviceSetting>> { return callbackFlow { val listener = @@ -202,51 +251,28 @@ class DeviceSettingServiceConnection( .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) } - private fun getService(intent: Intent): Flow<IBinder?> { + private fun <T : IInterface> getService( + intent: Intent, + transform: ((IBinder) -> T), + ): Flow<ServiceConnectionStatus<T>> { return callbackFlow { val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { - launch { send(service) } + launch { send(ServiceConnectionStatus.Connected(transform(service))) } } override fun onServiceDisconnected(name: ComponentName?) { - launch { send(null) } + launch { send(ServiceConnectionStatus.Connecting) } } } if (!context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)) { - launch { send(null) } + launch { send(ServiceConnectionStatus.Failed) } } awaitClose { context.unbindService(serviceConnection) } } } - private fun getConfigServiceBindingIntent(cachedDevice: CachedBluetoothDevice): Flow<Intent> { - return callbackFlow { - val listener = - BluetoothAdapter.OnMetadataChangedListener { device, key, _ -> - if ( - key == METADATA_FAST_PAIR_CUSTOMIZED_FIELDS && - cachedDevice.device == device - ) { - launch { tryGetEndpointFromMetadata(cachedDevice)?.let { send(it) } } - } - } - bluetoothAdaptor.addOnMetadataChangedListener( - cachedDevice.device, - ConcurrentUtils.DIRECT_EXECUTOR, - listener, - ) - awaitClose { - bluetoothAdaptor.removeOnMetadataChangedListener(cachedDevice.device, listener) - } - } - .onStart { tryGetEndpointFromMetadata(cachedDevice)?.let { emit(it) } } - .distinctUntilChanged() - .map { it.toIntent() } - .flowOn(backgroundCoroutineContext) - } - private suspend fun tryGetEndpointFromMetadata(cachedDevice: CachedBluetoothDevice): EndPoint? = withContext(backgroundCoroutineContext) { val packageName = @@ -257,29 +283,31 @@ class DeviceSettingServiceConnection( val className = BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, - CONFIG_SERVICE_CLASS_NAME + CONFIG_SERVICE_CLASS_NAME, ) ?: return@withContext null val intentAction = BluetoothUtils.getFastPairCustomizedField( cachedDevice.device, - CONFIG_SERVICE_INTENT_ACTION + CONFIG_SERVICE_INTENT_ACTION, ) ?: return@withContext null EndPoint(packageName, className, intentAction) } - private inline fun <T> AtomicReference<T?>.computeIfAbsent(producer: () -> T): T? = - get() ?: producer().let { compareAndExchange(null, it) ?: it } - private inline fun deviceInfo(block: DeviceInfo.Builder.() -> Unit): DeviceInfo { return DeviceInfo.Builder().apply { block() }.build() } companion object { + const val TAG = "DeviceSettingSrvConn" const val METADATA_FAST_PAIR_CUSTOMIZED_FIELDS: Int = 25 const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME" const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS" const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION" - val services = ConcurrentHashMap<EndPoint, StateFlow<IDeviceSettingsProviderService?>>() + val services = + ConcurrentHashMap< + EndPoint, + StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>, + >() } } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconKeys.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconKeys.java index 0a0b65b50e8b..79dabf029c49 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconKeys.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconKeys.java @@ -37,7 +37,7 @@ class ZenIconKeys { * chosen one via Settings). */ static final ZenIcon.Key IMPLICIT_MODE_DEFAULT = ZenIcon.Key.forSystemResource( - R.drawable.ic_zen_mode_type_unknown); + R.drawable.ic_zen_mode_type_special_dnd); private static final ImmutableMap<Integer, ZenIcon.Key> TYPE_DEFAULTS = ImmutableMap.of( AutomaticZenRule.TYPE_UNKNOWN, diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatusTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatusTest.kt new file mode 100644 index 000000000000..aa22fac49cd8 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatusTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeviceSettingsProviderServiceStatusTest { + + @Test + fun parcelOperation() { + val item = + DeviceSettingsProviderServiceStatus( + enabled = true, + extras = Bundle().apply { putString("key1", "value1") }, + ) + + val fromParcel = writeAndRead(item) + + assertThat(fromParcel.enabled).isEqualTo(item.enabled) + assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1")) + } + + private fun writeAndRead( + item: DeviceSettingsProviderServiceStatus + ): DeviceSettingsProviderServiceStatus { + val parcel = Parcel.obtain() + item.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return DeviceSettingsProviderServiceStatus.CREATOR.createFromParcel(parcel) + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt index 95ee46e4fdb9..ce155b5c0fa4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt @@ -33,6 +33,7 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsProviderServiceStatus import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsConfigProviderService import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsProviderService @@ -47,10 +48,8 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -59,12 +58,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.eq -import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.doReturn import org.mockito.Mockito.verify @@ -85,9 +81,6 @@ class DeviceSettingRepositoryTest { @Mock private lateinit var configService: IDeviceSettingsConfigProviderService.Stub @Mock private lateinit var settingProviderService1: IDeviceSettingsProviderService.Stub @Mock private lateinit var settingProviderService2: IDeviceSettingsProviderService.Stub - @Captor - private lateinit var metadataChangeCaptor: - ArgumentCaptor<BluetoothAdapter.OnMetadataChangedListener> private lateinit var underTest: DeviceSettingRepository private val testScope = TestScope() @@ -153,6 +146,12 @@ class DeviceSettingRepositoryTest { fun getDeviceSettingsConfig_withMetadata_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) val config = underTest.getDeviceSettingsConfig(cachedDevice) @@ -161,32 +160,40 @@ class DeviceSettingRepositoryTest { } @Test - fun getDeviceSettingsConfig_waitMetadataChange_success() { + fun getDeviceSettingsConfig_noMetadata_returnNull() { testScope.runTest { - `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`( - bluetoothDevice.getMetadata( - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + bluetoothDevice.getMetadata( + DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) .thenReturn("".toByteArray()) + `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) - var config: DeviceSettingConfigModel? = null - val job = launch { config = underTest.getDeviceSettingsConfig(cachedDevice) } - delay(1000) - verify(bluetoothAdapter) - .addOnMetadataChangedListener( - eq(bluetoothDevice), any(), metadataChangeCaptor.capture()) - metadataChangeCaptor.value.onMetadataChanged( - bluetoothDevice, - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS, - BLUETOOTH_DEVICE_METADATA.toByteArray(), + val config = underTest.getDeviceSettingsConfig(cachedDevice) + + assertThat(config).isNull() + } + } + + @Test + fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() { + testScope.runTest { + `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(false) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) ) - `when`( - bluetoothDevice.getMetadata( - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) - .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray()) - job.join() - assertConfig(config!!, DEVICE_SETTING_CONFIG) + val config = underTest.getDeviceSettingsConfig(cachedDevice) + + assertThat(config).isNull() } } @@ -212,6 +219,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -234,6 +247,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -256,6 +275,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_HELP)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -299,6 +324,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest @@ -331,6 +362,12 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } + `when`(settingProviderService1.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) + `when`(settingProviderService2.serviceStatus).thenReturn( + DeviceSettingsProviderServiceStatus(true) + ) var setting: DeviceSettingModel? = null underTest diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index e64b0c6d8e74..14b0c252aff5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -359,7 +359,7 @@ public class ZenModeTest { } @Test - public void getIconKey_implicitModeWithoutCustomIcon_isSpecialIcon() { + public void getIconKey_implicitModeWithoutCustomIcon_isDndIcon() { ZenMode mode = new TestModeBuilder() .setId(ZenModeConfig.implicitRuleId("some.package")) .setPackage("some_package") @@ -370,7 +370,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( - com.android.internal.R.drawable.ic_zen_mode_type_unknown); + com.android.internal.R.drawable.ic_zen_mode_type_special_dnd); } private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) { diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index d3949769cd58..157af7d06fed 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -92,6 +92,7 @@ <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <uses-permission android:name="android.permission.MASTER_CLEAR" /> <uses-permission android:name="android.permission.VIBRATE" /> + <uses-permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS" /> <uses-permission android:name="android.permission.MANAGE_SENSOR_PRIVACY" /> <uses-permission android:name="android.permission.OBSERVE_SENSOR_PRIVACY" /> <uses-permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" /> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java index c2cf6e104a6a..c333a7a5e33e 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java @@ -16,7 +16,6 @@ package com.android.systemui.accessibility.accessibilitymenu.view; -import android.content.Context; import android.graphics.Rect; import android.view.LayoutInflater; import android.view.TouchDelegate; @@ -43,16 +42,14 @@ public class A11yMenuAdapter extends BaseAdapter { private final int mLargeTextSize; private final AccessibilityMenuService mService; - private final LayoutInflater mInflater; private final List<A11yMenuShortcut> mShortcutDataList; private final ShortcutDrawableUtils mShortcutDrawableUtils; public A11yMenuAdapter( AccessibilityMenuService service, - Context displayContext, List<A11yMenuShortcut> shortcutDataList) { + List<A11yMenuShortcut> shortcutDataList) { this.mService = service; this.mShortcutDataList = shortcutDataList; - mInflater = LayoutInflater.from(displayContext); mShortcutDrawableUtils = new ShortcutDrawableUtils(service); @@ -78,7 +75,8 @@ public class A11yMenuAdapter extends BaseAdapter { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { - convertView = mInflater.inflate(R.layout.grid_item, parent, false); + convertView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.grid_item, parent, false); configureShortcutSize(convertView, A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)); diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java index de3c47235f6f..448472d1b6e4 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java @@ -51,6 +51,7 @@ import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.UiContext; import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; import com.android.systemui.accessibility.accessibilitymenu.Flags; @@ -101,7 +102,6 @@ public class A11yMenuOverlayLayout { }; private final AccessibilityMenuService mService; - private final WindowManager mWindowManager; private final DisplayManager mDisplayManager; private ViewGroup mLayout; private WindowManager.LayoutParams mLayoutParameter; @@ -111,7 +111,6 @@ public class A11yMenuOverlayLayout { public A11yMenuOverlayLayout(AccessibilityMenuService service) { mService = service; - mWindowManager = mService.getSystemService(WindowManager.class); mDisplayManager = mService.getSystemService(DisplayManager.class); configureLayout(); mHandler = new Handler(Looper.getMainLooper()); @@ -134,8 +133,7 @@ public class A11yMenuOverlayLayout { int lastVisibilityState = View.GONE; if (mLayout != null) { lastVisibilityState = mLayout.getVisibility(); - mWindowManager.removeView(mLayout); - mLayout = null; + clearLayout(); } if (mLayoutParameter == null) { @@ -143,14 +141,15 @@ public class A11yMenuOverlayLayout { } final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY); - final Context context = mService.createDisplayContext(display).createWindowContext( - TYPE_ACCESSIBILITY_OVERLAY, null); - mLayout = new A11yMenuFrameLayout(context); - updateLayoutPosition(); - inflateLayoutAndSetOnTouchListener(mLayout, context); - mA11yMenuViewPager = new A11yMenuViewPager(mService, context); + final Context uiContext = mService.createWindowContext( + display, TYPE_ACCESSIBILITY_OVERLAY, /* options= */null); + final WindowManager windowManager = uiContext.getSystemService(WindowManager.class); + mLayout = new A11yMenuFrameLayout(uiContext); + updateLayoutPosition(uiContext); + inflateLayoutAndSetOnTouchListener(mLayout, uiContext); + mA11yMenuViewPager = new A11yMenuViewPager(mService); mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex); - mWindowManager.addView(mLayout, mLayoutParameter); + windowManager.addView(mLayout, mLayoutParameter); mLayout.setVisibility(lastVisibilityState); mA11yMenuViewPager.updateFooterState(); @@ -159,7 +158,11 @@ public class A11yMenuOverlayLayout { public void clearLayout() { if (mLayout != null) { - mWindowManager.removeView(mLayout); + WindowManager windowManager = + mLayout.getContext().getSystemService(WindowManager.class); + if (windowManager != null) { + windowManager.removeView(mLayout); + } mLayout.setOnTouchListener(null); mLayout = null; } @@ -170,8 +173,11 @@ public class A11yMenuOverlayLayout { if (mLayout == null || mLayoutParameter == null) { return; } - updateLayoutPosition(); - mWindowManager.updateViewLayout(mLayout, mLayoutParameter); + updateLayoutPosition(mLayout.getContext()); + WindowManager windowManager = mLayout.getContext().getSystemService(WindowManager.class); + if (windowManager != null) { + windowManager.updateViewLayout(mLayout, mLayoutParameter); + } } private void initLayoutParams() { @@ -183,8 +189,8 @@ public class A11yMenuOverlayLayout { mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name)); } - private void inflateLayoutAndSetOnTouchListener(ViewGroup view, Context displayContext) { - LayoutInflater inflater = LayoutInflater.from(displayContext); + private void inflateLayoutAndSetOnTouchListener(ViewGroup view, @UiContext Context uiContext) { + LayoutInflater inflater = LayoutInflater.from(uiContext); inflater.inflate(R.layout.paged_menu, view); view.setOnTouchListener(mService); } @@ -238,7 +244,11 @@ public class A11yMenuOverlayLayout { } /** Updates a11y menu layout position by configuring layout params. */ - private void updateLayoutPosition() { + private void updateLayoutPosition(@UiContext @NonNull Context uiContext) { + WindowManager windowManager = uiContext.getSystemService(WindowManager.class); + if (windowManager == null) { + return; + } final Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY); final Configuration configuration = mService.getResources().getConfiguration(); final int orientation = configuration.orientation; @@ -276,14 +286,13 @@ public class A11yMenuOverlayLayout { mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT; mLayout.setBackgroundResource(R.drawable.shadow_0deg); } - // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom // navigation bar window. - updateLayoutByWindowInsetsIfNeeded(); + updateLayoutByWindowInsetsIfNeeded(windowManager); mLayout.setOnApplyWindowInsetsListener( (view, insets) -> { - if (updateLayoutByWindowInsetsIfNeeded()) { - mWindowManager.updateViewLayout(mLayout, mLayoutParameter); + if (updateLayoutByWindowInsetsIfNeeded(windowManager)) { + windowManager.updateViewLayout(mLayout, mLayoutParameter); } return view.onApplyWindowInsets(insets); }); @@ -295,9 +304,9 @@ public class A11yMenuOverlayLayout { * This method adjusts the layout position and size to * make a11y menu not to overlap navigation bar window. */ - private boolean updateLayoutByWindowInsetsIfNeeded() { + private boolean updateLayoutByWindowInsetsIfNeeded(@NonNull WindowManager windowManager) { boolean shouldUpdateLayout = false; - WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); + WindowMetrics windowMetrics = windowManager.getCurrentWindowMetrics(); Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility( WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); int xOffset = max(windowInsets.left, windowInsets.right); @@ -396,7 +405,7 @@ public class A11yMenuOverlayLayout { } private class A11yMenuFrameLayout extends FrameLayout { - A11yMenuFrameLayout(@NonNull Context context) { + A11yMenuFrameLayout(@UiContext @NonNull Context context) { super(context); } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java index 08bbf192591e..a29ce82dc132 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java @@ -146,12 +146,8 @@ public class A11yMenuViewPager { /** The container layout for a11y menu. */ private ViewGroup mA11yMenuLayout; - /** Display context for inflating views. */ - private Context mDisplayContext; - - public A11yMenuViewPager(AccessibilityMenuService service, Context displayContext) { + public A11yMenuViewPager(AccessibilityMenuService service) { this.mService = service; - this.mDisplayContext = displayContext; } /** @@ -289,7 +285,8 @@ public class A11yMenuViewPager { footerLayout.getLayoutParams().height = (int) (footerLayout.getLayoutParams().height / densityScale); // Adjust the view pager height for system bar and display cutout insets. - WindowManager windowManager = mService.getSystemService(WindowManager.class); + WindowManager windowManager = mA11yMenuLayout.getContext() + .getSystemService(WindowManager.class); WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics(); Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility( WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java index 43ec9561f4db..152ff68b8a07 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java @@ -57,7 +57,7 @@ class ViewPagerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { A11yMenuAdapter adapter = new A11yMenuAdapter( - mService, holder.itemView.getContext(), mShortcutList.get(position)); + mService, mShortcutList.get(position)); GridView gridView = (GridView) holder.itemView; gridView.setNumColumns(A11yMenuViewPager.GridViewParams.getGridColumnCount(mService)); gridView.setAdapter(adapter); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index a450d793fb1c..cf13621c2c1b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -606,16 +606,6 @@ flag { } flag { - name: "screenshot_private_profile_behavior_fix" - namespace: "systemui" - description: "Private profile support for screenshots" - bug: "327613051" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "screenshot_save_image_exporter" namespace: "systemui" description: "Save all screenshots using ImageExporter" @@ -1376,6 +1366,16 @@ flag { } flag { + name: "notify_password_text_view_user_activity_in_background" + namespace: "systemui" + description: "Decide whether to notify the user activity in password text view, to power manager in the background thread." + bug: "346882515" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "face_message_defer_update" namespace: "systemui" description: "Only analyze the last n frames when determining whether to defer a face auth help message like low light" diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt index 2b1268e40f00..5b368df9d0ef 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/BouncerSceneModule.kt @@ -17,7 +17,7 @@ package com.android.systemui.scene import com.android.systemui.bouncer.ui.composable.BouncerScene -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.ui.composable.Scene import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/CommunalSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/CommunalSceneModule.kt index 94b5db2535ab..74ce4bb754ba 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/CommunalSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/CommunalSceneModule.kt @@ -17,7 +17,7 @@ package com.android.systemui.scene import com.android.systemui.communal.ui.compose.CommunalScene -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.ui.composable.Scene import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/GoneSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/GoneSceneModule.kt index bc3fef15e577..871ade90a5dd 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/GoneSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/GoneSceneModule.kt @@ -16,8 +16,8 @@ package com.android.systemui.scene -import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.ui.composable.GoneScene +import com.android.systemui.scene.ui.composable.Scene import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt index 72965fb24d89..bfeaf928dfe8 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt @@ -27,7 +27,7 @@ import com.android.systemui.keyguard.ui.composable.LockscreenScene import com.android.systemui.keyguard.ui.composable.LockscreenSceneBlueprintModule import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.ui.composable.Scene import dagger.Binds import dagger.Module import dagger.Provides diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeOverlayModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeOverlayModule.kt new file mode 100644 index 000000000000..e55520a09103 --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeOverlayModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.scene + +import com.android.systemui.notifications.ui.composable.NotificationsShadeOverlay +import com.android.systemui.scene.ui.composable.Overlay +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet + +@Module +interface NotificationsShadeOverlayModule { + + @Binds @IntoSet fun notificationsShade(overlay: NotificationsShadeOverlay): Overlay +} diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt index 9b736b8edcbf..c58df35fd6cb 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/NotificationsShadeSceneModule.kt @@ -17,7 +17,7 @@ package com.android.systemui.scene import com.android.systemui.notifications.ui.composable.NotificationsShadeScene -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.ui.composable.Scene import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsSceneModule.kt index ee1f5259ec69..d55210da3739 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsSceneModule.kt @@ -17,7 +17,7 @@ package com.android.systemui.scene import com.android.systemui.qs.ui.composable.QuickSettingsScene -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.ui.composable.Scene import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeOverlayModule.kt index 3da6a02d08d3..bc4adf93c556 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeOverlayModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,14 +14,16 @@ * limitations under the License. */ -package com.android.systemui.scene.ui.composable +package com.android.systemui.scene -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.android.compose.animation.scene.SceneScope -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.qs.ui.composable.QuickSettingsShadeOverlay +import com.android.systemui.scene.ui.composable.Overlay +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet -/** Compose-capable extension of [Scene]. */ -interface ComposableScene : Scene { - @Composable fun SceneScope.Content(modifier: Modifier) +@Module +interface QuickSettingsShadeOverlayModule { + + @Binds @IntoSet fun quickSettingsShade(overlay: QuickSettingsShadeOverlay): Overlay } diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt index 3d7401d8f263..5bb6ae46bae2 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt @@ -17,7 +17,7 @@ package com.android.systemui.scene import com.android.systemui.qs.ui.composable.QuickSettingsShadeScene -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.ui.composable.Scene import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ShadeSceneModule.kt index c655d6ba3f2f..186914f56b06 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ShadeSceneModule.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/ShadeSceneModule.kt @@ -16,7 +16,7 @@ package com.android.systemui.scene -import com.android.systemui.scene.shared.model.Scene +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.ui.composable.ShadeScene import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index d164eab5afeb..9f78d69505de 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -528,7 +528,7 @@ private fun FoldAware( // Update state whenever currentSceneKey has changed. LaunchedEffect(state, currentSceneKey) { if (currentSceneKey != state.transitionState.currentScene) { - state.setTargetScene(currentSceneKey, coroutineScope = this) + state.setTargetScene(currentSceneKey, animationScope = this) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 270d751d2d3c..c5bb33c414b1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -34,7 +34,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.Scene import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -57,7 +57,7 @@ constructor( private val actionsViewModelFactory: BouncerSceneActionsViewModel.Factory, private val contentViewModelFactory: BouncerSceneContentViewModel.Factory, private val dialogFactory: BouncerDialogFactory, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.Bouncer private val actionsViewModel: BouncerSceneActionsViewModel by lazy { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index 54ffcf475680..f658169a24ff 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -29,7 +29,7 @@ import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.statusbar.phone.SystemUIDialogFactory import javax.inject.Inject import kotlinx.coroutines.awaitCancellation @@ -46,7 +46,7 @@ constructor( private val dialogFactory: SystemUIDialogFactory, private val interactionHandler: WidgetInteractionHandler, private val widgetSection: CommunalAppWidgetSection, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.Communal override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt index 04bcc3624532..c60e11e585f2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt @@ -73,8 +73,7 @@ fun AlternateBouncer( initialValue = null ) - // TODO (b/353955910): back handling doesn't work - BackHandler { alternateBouncerDependencies.viewModel.onBackRequested() } + BackHandler(enabled = isVisible) { alternateBouncerDependencies.viewModel.onBackRequested() } AnimatedVisibility( visible = isVisible, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt index 2029e9e7f139..5f600d3002cc 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt @@ -22,13 +22,13 @@ import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.compose.animation.scene.animateSceneFloatAsState +import com.android.compose.animation.scene.animateContentFloatAsState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneActionsViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.Scene import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -40,7 +40,7 @@ class LockscreenScene constructor( actionsViewModelFactory: LockscreenSceneActionsViewModel.Factory, private val lockscreenContent: Lazy<LockscreenContent>, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.Lockscreen private val actionsViewModel: LockscreenSceneActionsViewModel by lazy { @@ -70,7 +70,7 @@ private fun SceneScope.LockscreenScene( lockscreenContent: Lazy<LockscreenContent>, modifier: Modifier = Modifier, ) { - animateSceneFloatAsState( + animateContentFloatAsState( value = QuickSettings.SharedValues.SquishinessValues.LockscreenSceneStarting, key = QuickSettings.SharedValues.TilesSquishiness, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 0eeb79b959ce..97d89a2631bf 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -93,7 +93,7 @@ constructor( // Update state whenever currentSceneKey has changed. LaunchedEffect(state, currentScene) { if (currentScene != state.transitionState.currentScene) { - state.setTargetScene(currentScene, coroutineScope = this) + state.setTargetScene(currentScene, animationScope = this) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt new file mode 100644 index 000000000000..e4c611ee0eb2 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 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.notifications.ui.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ContentScope +import com.android.systemui.battery.BatteryMeterViewController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel +import com.android.systemui.scene.session.ui.composable.SaveableSession +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.composable.ExpandedShadeHeader +import com.android.systemui.shade.ui.composable.OverlayShade +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.phone.ui.TintedIconManager +import dagger.Lazy +import javax.inject.Inject + +@SysUISingleton +class NotificationsShadeOverlay +@Inject +constructor( + private val actionsViewModelFactory: NotificationsShadeOverlayActionsViewModel.Factory, + private val contentViewModelFactory: NotificationsShadeOverlayContentViewModel.Factory, + private val tintedIconManagerFactory: TintedIconManager.Factory, + private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, + private val statusBarIconController: StatusBarIconController, + private val shadeSession: SaveableSession, + private val stackScrollView: Lazy<NotificationScrollView>, +) : Overlay { + + override val key = Overlays.NotificationsShade + + private val actionsViewModel: NotificationsShadeOverlayActionsViewModel by lazy { + actionsViewModelFactory.create() + } + + override suspend fun activate(): Nothing { + actionsViewModel.activate() + } + + @Composable + override fun ContentScope.Content( + modifier: Modifier, + ) { + val viewModel = + rememberViewModel("NotificationsShadeOverlay-viewModel") { + contentViewModelFactory.create() + } + val placeholderViewModel = + rememberViewModel("NotificationsShadeOverlay-notifPlaceholderViewModel") { + viewModel.notificationsPlaceholderViewModelFactory.create() + } + + OverlayShade( + modifier = modifier, + onScrimClicked = viewModel::onScrimClicked, + ) { + Column { + ExpandedShadeHeader( + viewModelFactory = viewModel.shadeHeaderViewModelFactory, + createTintedIconManager = tintedIconManagerFactory::create, + createBatteryMeterViewController = batteryMeterViewControllerFactory::create, + statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + NotificationScrollingStack( + shadeSession = shadeSession, + stackScrollView = stackScrollView.get(), + viewModel = placeholderViewModel, + maxScrimTop = { 0f }, + shouldPunchHoleBehindScrim = false, + shouldFillMaxSize = false, + shouldReserveSpaceForNavBar = false, + shadeMode = ShadeMode.Dual, + modifier = Modifier.fillMaxWidth(), + ) + + // Communicates the bottom position of the drawable area within the shade to NSSL. + NotificationStackCutoffGuideline( + stackScrollView = stackScrollView.get(), + viewModel = placeholderViewModel, + ) + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index e9c96eaafc74..ea3f066960c1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -27,24 +27,21 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.ui.composable.LockscreenContent import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.OverlayShade -import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager import dagger.Lazy -import java.util.Optional import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -53,7 +50,6 @@ class NotificationsShadeScene @Inject constructor( private val actionsViewModelFactory: NotificationsShadeSceneActionsViewModel.Factory, - private val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, private val tintedIconManagerFactory: TintedIconManager.Factory, @@ -61,8 +57,7 @@ constructor( private val statusBarIconController: StatusBarIconController, private val shadeSession: SaveableSession, private val stackScrollView: Lazy<NotificationScrollView>, - private val lockscreenContent: Lazy<Optional<LockscreenContent>>, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.NotificationsShade @@ -88,8 +83,7 @@ constructor( OverlayShade( modifier = modifier, - viewModelFactory = overlayShadeViewModelFactory, - lockscreenContent = lockscreenContent, + onScrimClicked = {}, ) { Column { ExpandedShadeHeader( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index d372577142c1..373383f0a2fe 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -98,7 +98,7 @@ import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneContentViewModel import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.ExpandedShadeHeader @@ -113,9 +113,11 @@ import dagger.Lazy import javax.inject.Inject import javax.inject.Named import kotlin.math.roundToInt +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow /** The Quick Settings (AKA "QS") scene shows the quick setting tiles. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class QuickSettingsScene @Inject @@ -130,7 +132,7 @@ constructor( private val statusBarIconController: StatusBarIconController, private val mediaCarouselController: MediaCarouselController, @Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.QuickSettings private val actionsViewModel: QuickSettingsSceneActionsViewModel by lazy { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt new file mode 100644 index 000000000000..988c712b7980 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 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.ui.composable + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.ContentScope +import com.android.systemui.battery.BatteryMeterViewController +import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer +import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.qs.panels.ui.compose.EditMode +import com.android.systemui.qs.panels.ui.compose.TileGrid +import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeOverlayActionsViewModel +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeOverlayContentViewModel +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.shade.ui.composable.ExpandedShadeHeader +import com.android.systemui.shade.ui.composable.OverlayShade +import com.android.systemui.statusbar.phone.ui.StatusBarIconController +import com.android.systemui.statusbar.phone.ui.TintedIconManager +import javax.inject.Inject + +@SysUISingleton +class QuickSettingsShadeOverlay +@Inject +constructor( + private val actionsViewModelFactory: QuickSettingsShadeOverlayActionsViewModel.Factory, + private val contentViewModelFactory: QuickSettingsShadeOverlayContentViewModel.Factory, + private val tintedIconManagerFactory: TintedIconManager.Factory, + private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, + private val statusBarIconController: StatusBarIconController, +) : Overlay { + + override val key = Overlays.QuickSettingsShade + + private val actionsViewModel: QuickSettingsShadeOverlayActionsViewModel by lazy { + actionsViewModelFactory.create() + } + + override suspend fun activate(): Nothing { + actionsViewModel.activate() + } + + @Composable + override fun ContentScope.Content( + modifier: Modifier, + ) { + val viewModel = + rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() } + + OverlayShade( + modifier = modifier, + onScrimClicked = viewModel::onScrimClicked, + ) { + Column { + ExpandedShadeHeader( + viewModelFactory = viewModel.shadeHeaderViewModelFactory, + createTintedIconManager = tintedIconManagerFactory::create, + createBatteryMeterViewController = batteryMeterViewControllerFactory::create, + statusBarIconController = statusBarIconController, + modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding), + ) + + ShadeBody( + viewModel = viewModel.quickSettingsContainerViewModel, + ) + } + } + } +} + +@Composable +fun ShadeBody( + viewModel: QuickSettingsContainerViewModel, +) { + val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle() + + AnimatedContent( + targetState = isEditing, + transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) } + ) { editing -> + if (editing) { + EditMode( + viewModel = viewModel.editModeViewModel, + modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding) + ) + } else { + QuickSettingsLayout( + viewModel = viewModel, + modifier = Modifier.sysuiResTag("quick_settings_panel") + ) + } + } +} + +@Composable +private fun QuickSettingsLayout( + viewModel: QuickSettingsContainerViewModel, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(QuickSettingsShade.Dimensions.Padding), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding), + ) { + BrightnessSliderContainer( + viewModel = viewModel.brightnessSliderViewModel, + modifier = + Modifier.fillMaxWidth() + .height(QuickSettingsShade.Dimensions.BrightnessSliderHeight), + ) + TileGrid( + viewModel = viewModel.tileGridViewModel, + modifier = + Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight), + viewModel.editModeViewModel::startEditing, + ) + } +} + +object QuickSettingsShade { + + object Dimensions { + val Padding = 16.dp + val BrightnessSliderHeight = 64.dp + val GridMaxHeight = 800.dp + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt index 90d7da65b29f..9316eb90a7a2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt @@ -16,51 +16,26 @@ package com.android.systemui.qs.ui.composable -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.battery.BatteryMeterViewController -import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer -import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.ui.composable.LockscreenContent import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel -import com.android.systemui.qs.panels.ui.compose.EditMode -import com.android.systemui.qs.panels.ui.compose.TileGrid -import com.android.systemui.qs.ui.composable.QuickSettingsShade.Transitions.QuickSettingsLayoutEnter -import com.android.systemui.qs.ui.composable.QuickSettingsShade.Transitions.QuickSettingsLayoutExit -import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneActionsViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneContentViewModel import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager -import dagger.Lazy -import java.util.Optional import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -70,12 +45,11 @@ class QuickSettingsShadeScene constructor( private val actionsViewModelFactory: QuickSettingsShadeSceneActionsViewModel.Factory, private val contentViewModelFactory: QuickSettingsShadeSceneContentViewModel.Factory, - private val lockscreenContent: Lazy<Optional<LockscreenContent>>, private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, private val tintedIconManagerFactory: TintedIconManager.Factory, private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory, private val statusBarIconController: StatusBarIconController, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.QuickSettingsShade @@ -96,10 +70,10 @@ constructor( ) { val viewModel = rememberViewModel("QuickSettingsShadeScene") { contentViewModelFactory.create() } + OverlayShade( - viewModelFactory = viewModel.overlayShadeViewModelFactory, - lockscreenContent = lockscreenContent, modifier = modifier, + onScrimClicked = {}, ) { Column { ExpandedShadeHeader( @@ -117,68 +91,3 @@ constructor( } } } - -@Composable -fun ShadeBody( - viewModel: QuickSettingsContainerViewModel, -) { - val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle() - - AnimatedContent( - targetState = isEditing, - transitionSpec = { QuickSettingsLayoutEnter togetherWith QuickSettingsLayoutExit } - ) { editing -> - if (editing) { - EditMode( - viewModel = viewModel.editModeViewModel, - modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding) - ) - } else { - QuickSettingsLayout( - viewModel = viewModel, - modifier = Modifier.sysuiResTag("quick_settings_panel") - ) - } - } -} - -@Composable -private fun QuickSettingsLayout( - viewModel: QuickSettingsContainerViewModel, - modifier: Modifier = Modifier, -) { - Column( - verticalArrangement = Arrangement.spacedBy(QuickSettingsShade.Dimensions.Padding), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding), - ) { - BrightnessSliderContainer( - viewModel = viewModel.brightnessSliderViewModel, - modifier = - Modifier.fillMaxWidth() - .height(QuickSettingsShade.Dimensions.BrightnessSliderHeight), - ) - TileGrid( - viewModel = viewModel.tileGridViewModel, - modifier = - Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight), - viewModel.editModeViewModel::startEditing, - ) - } -} - -object QuickSettingsShade { - - object Dimensions { - val Padding = 16.dp - val BrightnessSliderHeight = 64.dp - val GridMaxHeight = 800.dp - } - - object Transitions { - val QuickSettingsLayoutEnter: EnterTransition = fadeIn(tween(500)) - val QuickSettingsLayoutExit: ExitTransition = fadeOut(tween(500)) - val QuickSettingsEditorEnter: EnterTransition = fadeIn(tween(500)) - val QuickSettingsEditorExit: ExitTransition = fadeOut(tween(500)) - } -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index cbbace47ecc4..6fb4724426fc 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult -import com.android.compose.animation.scene.animateSceneDpAsState -import com.android.compose.animation.scene.animateSceneFloatAsState +import com.android.compose.animation.scene.animateContentDpAsState +import com.android.compose.animation.scene.animateContentFloatAsState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.rememberViewModel @@ -51,7 +51,7 @@ constructor( private val notificationStackScrolLView: Lazy<NotificationScrollView>, private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, private val viewModelFactory: GoneSceneActionsViewModel.Factory, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.Gone private val actionsViewModel: GoneSceneActionsViewModel by lazy { viewModelFactory.create() } @@ -67,11 +67,11 @@ constructor( override fun SceneScope.Content( modifier: Modifier, ) { - animateSceneFloatAsState( + animateContentFloatAsState( value = QuickSettings.SharedValues.SquishinessValues.GoneSceneStarting, key = QuickSettings.SharedValues.TilesSquishiness, ) - animateSceneDpAsState(value = Default, key = MediaLandscapeTopOffset, canOverflow = false) + animateContentDpAsState(value = Default, key = MediaLandscapeTopOffset, canOverflow = false) Spacer(modifier.fillMaxSize()) SnoozeableHeadsUpNotificationSpace( stackScrollView = notificationStackScrolLView.get(), diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt index 8e2e8a1d521b..5319ec345d00 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,9 +14,12 @@ * limitations under the License. */ -package com.android.systemui.scene.shared.model +package com.android.systemui.scene.ui.composable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.lifecycle.Activatable @@ -56,4 +59,6 @@ interface Scene : Activatable { * current scene is this one. */ val destinationScenes: Flow<Map<UserAction, UserActionResult>> + + @Composable fun SceneScope.Content(modifier: Modifier) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index f9723d99656b..851fa3ff005e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -54,11 +54,11 @@ import kotlinx.coroutines.flow.collectLatest * containers. * * @param viewModel The UI state holder for this container. - * @param sceneByKey Mapping of [ComposableScene] by [SceneKey], ordered by z-order such that the - * last scene is rendered on top of all other scenes. It's critical that this map contains exactly - * and only the scenes on this container. In other words: (a) there should be no scene in this map - * that is not in the configuration for this container and (b) all scenes in the configuration - * must have entries in this map. + * @param sceneByKey Mapping of [Scene] by [SceneKey], ordered by z-order such that the last scene + * is rendered on top of all other scenes. It's critical that this map contains exactly and only + * the scenes on this container. In other words: (a) there should be no scene in this map that is + * not in the configuration for this container and (b) all scenes in the configuration must have + * entries in this map. * @param overlayByKey Mapping of [Overlay] by [OverlayKey], ordered by z-order such that the last * overlay is rendered on top of all other overlays. It's critical that this map contains exactly * and only the overlays on this container. In other words: (a) there should be no overlay in this @@ -69,7 +69,7 @@ import kotlinx.coroutines.flow.collectLatest @Composable fun SceneContainer( viewModel: SceneContainerViewModel, - sceneByKey: Map<SceneKey, ComposableScene>, + sceneByKey: Map<SceneKey, Scene>, overlayByKey: Map<OverlayKey, Overlay>, initialSceneKey: SceneKey, dataSourceDelegator: SceneDataSourceDelegator, @@ -123,16 +123,16 @@ fun SceneContainer( }, ) { SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) { - sceneByKey.forEach { (sceneKey, composableScene) -> + sceneByKey.forEach { (sceneKey, scene) -> scene( key = sceneKey, userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap()) ) { // Activate the scene. - LaunchedEffect(composableScene) { composableScene.activate() } + LaunchedEffect(scene) { scene.activate() } // Render the scene. - with(composableScene) { + with(scene) { this@scene.Content( modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(), ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 39fc7ef53235..a0ebca2bc379 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -1,13 +1,11 @@ package com.android.systemui.scene.ui.composable import androidx.compose.foundation.gestures.Orientation -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ProgressConverter import com.android.compose.animation.scene.transitions import com.android.systemui.bouncer.ui.composable.Bouncer import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.shared.model.TransitionKeys.OpenBottomShade import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition @@ -48,18 +46,8 @@ val SceneContainerTransitions = transitions { // Scene transitions from(Scenes.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() } - from(Scenes.Gone, to = Scenes.NotificationsShade) { - goneToNotificationsShadeTransition(Edge.Top) - } - from(Scenes.Gone, to = Scenes.NotificationsShade, key = OpenBottomShade) { - goneToNotificationsShadeTransition(Edge.Bottom) - } - from(Scenes.Gone, to = Scenes.QuickSettingsShade) { - goneToQuickSettingsShadeTransition(Edge.Top) - } - from(Scenes.Gone, to = Scenes.QuickSettingsShade, key = OpenBottomShade) { - goneToQuickSettingsShadeTransition(Edge.Bottom) - } + from(Scenes.Gone, to = Scenes.NotificationsShade) { goneToNotificationsShadeTransition() } + from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() } from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() } from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() } from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt index e12a8bde7c8f..6738b97de015 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt @@ -69,7 +69,7 @@ class SceneTransitionLayoutDataSource( state.setTargetScene( targetScene = toScene, transitionKey = transitionKey, - coroutineScope = coroutineScope, + animationScope = coroutineScope, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToNotificationsShadeTransition.kt index fb41374bfba8..48ec198a790a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToNotificationsShadeTransition.kt @@ -16,12 +16,10 @@ package com.android.systemui.scene.ui.composable.transitions -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder fun TransitionBuilder.goneToNotificationsShadeTransition( - edge: Edge = Edge.Top, durationScale: Double = 1.0, ) { - toNotificationsShadeTransition(edge, durationScale) + toNotificationsShadeTransition(durationScale) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index 05949b20fde2..337f53a58844 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -19,12 +19,9 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance -import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.Shade @@ -32,11 +29,6 @@ import com.android.systemui.shade.ui.composable.ShadeHeader import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition( - /** - * The edge where the shade will animate from. This is statically determined (i.e. doesn't - * change during runtime). - */ - edge: Edge = Edge.Top, durationScale: Double = 1.0, ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) @@ -45,17 +37,11 @@ fun TransitionBuilder.toNotificationsShadeTransition( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, ) - distance = - object : UserActionDistance { - override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, - orientation: Orientation, - ): Float { - return fromSceneSize.height.toFloat() * 2 / 3f - } - } + distance = UserActionDistance { fromSceneSize, orientation -> + fromSceneSize.height.toFloat() * 2 / 3f + } - translate(OverlayShade.Elements.Panel, edge) + translate(OverlayShade.Elements.Panel, Edge.Top) fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 595bbb035f21..89222246b4eb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -40,56 +40,30 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.SceneScope import com.android.compose.windowsizeclass.LocalWindowSizeClass -import com.android.systemui.keyguard.ui.composable.LockscreenContent -import com.android.systemui.lifecycle.rememberViewModel -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.shared.model.ShadeAlignment -import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel -import com.android.systemui.util.kotlin.getOrNull -import dagger.Lazy -import java.util.Optional - -/** The overlay shade renders a lightweight shade UI container on top of a background scene. */ + +/** Renders a lightweight shade UI container, as an overlay. */ @Composable fun SceneScope.OverlayShade( - viewModelFactory: OverlayShadeViewModel.Factory, - lockscreenContent: Lazy<Optional<LockscreenContent>>, + onScrimClicked: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - val viewModel = rememberViewModel("OverlayShade") { viewModelFactory.create() } - val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle() - Box(modifier) { - if (backgroundScene == Scenes.Lockscreen) { - // Lockscreen content is optionally injected, because variants of System UI without a - // lockscreen cannot provide it. - val lockscreenContentOrNull = lockscreenContent.get().getOrNull() - lockscreenContentOrNull?.apply { Content(Modifier.fillMaxSize()) } - } - - Scrim(onClicked = viewModel::onScrimClicked) + Scrim(onClicked = onScrimClicked) Box( modifier = Modifier.fillMaxSize().panelPadding(), - contentAlignment = - if (viewModel.panelAlignment == ShadeAlignment.Top) { - Alignment.TopEnd - } else { - Alignment.BottomEnd - }, + contentAlignment = Alignment.TopEnd, ) { Panel( modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index b7c6edce12d7..5fcf5226a585 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -30,7 +30,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -47,8 +47,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.CompositingStrategy @@ -99,14 +100,13 @@ import com.android.systemui.notifications.ui.composable.NotificationScrollingSta import com.android.systemui.notifications.ui.composable.NotificationStackCutoffGuideline import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility import com.android.systemui.qs.ui.composable.BrightnessMirror -import com.android.systemui.qs.ui.composable.QSMediaMeasurePolicy import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.InQQS import com.android.systemui.res.R import com.android.systemui.scene.session.ui.composable.SaveableSession import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel @@ -120,6 +120,7 @@ import dagger.Lazy import javax.inject.Inject import javax.inject.Named import kotlin.math.roundToInt +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow object Shade { @@ -146,6 +147,7 @@ object Shade { } /** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class ShadeScene @Inject @@ -161,7 +163,7 @@ constructor( private val mediaCarouselController: MediaCarouselController, @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost, @Named(QS_PANEL) private val qsMediaHost: MediaHost, -) : ExclusiveActivatable(), ComposableScene { +) : ExclusiveActivatable(), Scene { override val key = Scenes.Shade @@ -269,13 +271,14 @@ private fun SceneScope.SingleShade( shadeSession: SaveableSession, ) { val cutoutLocation = LocalDisplayCutout.current.location + val cutoutInsets = WindowInsets.Companion.displayCutout val isLandscape = LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact val usingCollapsedLandscapeMedia = Utils.useCollapsedMediaInLandscape(LocalContext.current.resources) val isExpanded = !usingCollapsedLandscapeMedia || !isLandscape mediaHost.expansion = if (isExpanded) EXPANDED else COLLAPSED - val maxNotifScrimTop = remember { mutableStateOf(0f) } + var maxNotifScrimTop by remember { mutableIntStateOf(0) } val tileSquishiness by animateSceneFloatAsState( value = 1f, @@ -301,6 +304,24 @@ private fun SceneScope.SingleShade( viewModel.qsSceneAdapter, ) } + val shadeMeasurePolicy = + remember(mediaInRow) { + SingleShadeMeasurePolicy( + isMediaInRow = mediaInRow, + mediaOffset = { mediaOffset.roundToPx() }, + onNotificationsTopChanged = { maxNotifScrimTop = it }, + mediaZIndex = { + if (MediaContentPicker.shouldElevateMedia(layoutState)) 1f else 0f + }, + cutoutInsetsProvider = { + if (cutoutLocation == CutoutLocation.CENTER) { + null + } else { + cutoutInsets + } + } + ) + } Box( modifier = @@ -318,101 +339,54 @@ private fun SceneScope.SingleShade( .background(colorResource(R.color.shade_scrim_background_dark)), ) Layout( - contents = - listOf( - { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier.fillMaxWidth() - .thenIf(isEmptySpaceClickable) { - Modifier.clickable( - onClick = { viewModel.onEmptySpaceClicked() } - ) - } - .thenIf(cutoutLocation != CutoutLocation.CENTER) { - Modifier.displayCutoutPadding() - }, - ) { - CollapsedShadeHeader( - viewModelFactory = viewModel.shadeHeaderViewModelFactory, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, - ) - - val content: @Composable () -> Unit = { - Box( - Modifier.element(QuickSettings.Elements.QuickQuickSettings) - .layoutId(QSMediaMeasurePolicy.LayoutId.QS) - ) { - QuickSettings( - viewModel.qsSceneAdapter, - { viewModel.qsSceneAdapter.qqsHeight }, - isSplitShade = false, - squishiness = { tileSquishiness }, - ) - } - - ShadeMediaCarousel( - isVisible = isMediaVisible, - mediaHost = mediaHost, - mediaOffsetProvider = mediaOffsetProvider, - modifier = - Modifier.layoutId(QSMediaMeasurePolicy.LayoutId.Media), - carouselController = mediaCarouselController, - ) - } - val landscapeQsMediaMeasurePolicy = remember { - QSMediaMeasurePolicy( - { viewModel.qsSceneAdapter.qqsHeight }, - { mediaOffset.roundToPx() }, - ) - } - if (mediaInRow) { - Layout( - content = content, - measurePolicy = landscapeQsMediaMeasurePolicy, - ) - } else { - content() - } - } - }, - { - NotificationScrollingStack( - shadeSession = shadeSession, - stackScrollView = notificationStackScrollView, - viewModel = notificationsPlaceholderViewModel, - maxScrimTop = { maxNotifScrimTop.value }, - shadeMode = ShadeMode.Single, - shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, - onEmptySpaceClick = - viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, - ) - }, + modifier = + Modifier.thenIf(isEmptySpaceClickable) { + Modifier.clickable { viewModel.onEmptySpaceClicked() } + }, + content = { + CollapsedShadeHeader( + viewModelFactory = viewModel.shadeHeaderViewModelFactory, + createTintedIconManager = createTintedIconManager, + createBatteryMeterViewController = createBatteryMeterViewController, + statusBarIconController = statusBarIconController, + modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), ) - ) { measurables, constraints -> - check(measurables.size == 2) - check(measurables[0].size == 1) - check(measurables[1].size == 1) - val quickSettingsPlaceable = measurables[0][0].measure(constraints) - val notificationsPlaceable = measurables[1][0].measure(constraints) + Box( + Modifier.element(QuickSettings.Elements.QuickQuickSettings) + .layoutId(SingleShadeMeasurePolicy.LayoutId.QuickSettings) + ) { + QuickSettings( + viewModel.qsSceneAdapter, + { viewModel.qsSceneAdapter.qqsHeight }, + isSplitShade = false, + squishiness = { tileSquishiness }, + ) + } - maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat() + ShadeMediaCarousel( + isVisible = isMediaVisible, + isInRow = mediaInRow, + mediaHost = mediaHost, + mediaOffsetProvider = mediaOffsetProvider, + carouselController = mediaCarouselController, + modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Media), + ) - layout(constraints.maxWidth, constraints.maxHeight) { - val qsZIndex = - if (MediaContentPicker.shouldElevateMedia(layoutState)) { - 1f - } else { - 0f - } - quickSettingsPlaceable.placeRelative(x = 0, y = 0, zIndex = qsZIndex) - notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt()) - } - } + NotificationScrollingStack( + shadeSession = shadeSession, + stackScrollView = notificationStackScrollView, + viewModel = notificationsPlaceholderViewModel, + maxScrimTop = { maxNotifScrimTop.toFloat() }, + shadeMode = ShadeMode.Single, + shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim, + onEmptySpaceClick = + viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable }, + modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Notifications), + ) + }, + measurePolicy = shadeMeasurePolicy, + ) Box( modifier = Modifier.align(Alignment.BottomCenter) @@ -600,6 +574,7 @@ private fun SceneScope.SplitShade( ShadeMediaCarousel( isVisible = isMediaVisible, + isInRow = false, mediaHost = mediaHost, mediaOffsetProvider = mediaOffsetProvider, modifier = @@ -657,6 +632,7 @@ private fun SceneScope.SplitShade( @Composable private fun SceneScope.ShadeMediaCarousel( isVisible: Boolean, + isInRow: Boolean, mediaHost: MediaHost, carouselController: MediaCarouselController, mediaOffsetProvider: ShadeMediaOffsetProvider, @@ -668,7 +644,7 @@ private fun SceneScope.ShadeMediaCarousel( mediaHost = mediaHost, carouselController = carouselController, offsetProvider = - if (MediaContentPicker.shouldElevateMedia(layoutState)) { + if (isInRow || MediaContentPicker.shouldElevateMedia(layoutState)) { null } else { { mediaOffsetProvider.offset } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt new file mode 100644 index 000000000000..6275ac396628 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/SingleShadeMeasurePolicy.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 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.shade.ui.composable + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.offset +import androidx.compose.ui.util.fastFirst +import androidx.compose.ui.util.fastFirstOrNull +import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy.LayoutId +import kotlin.math.max + +/** + * Lays out elements from the [LayoutId] in the shade. This policy supports the case when the QS and + * UMO share the same row and when they should be one below another. + */ +class SingleShadeMeasurePolicy( + private val isMediaInRow: Boolean, + private val mediaOffset: MeasureScope.() -> Int, + private val onNotificationsTopChanged: (Int) -> Unit, + private val mediaZIndex: () -> Float, + private val cutoutInsetsProvider: () -> WindowInsets?, +) : MeasurePolicy { + + enum class LayoutId { + QuickSettings, + Media, + Notifications, + ShadeHeader, + } + + override fun MeasureScope.measure( + measurables: List<Measurable>, + constraints: Constraints, + ): MeasureResult { + val cutoutInsets: WindowInsets? = cutoutInsetsProvider() + val constraintsWithCutout = applyCutout(constraints, cutoutInsets) + val insetsLeft = cutoutInsets?.getLeft(this, layoutDirection) ?: 0 + val insetsTop = cutoutInsets?.getTop(this) ?: 0 + + val shadeHeaderPlaceable = + measurables + .fastFirst { it.layoutId == LayoutId.ShadeHeader } + .measure(constraintsWithCutout) + val mediaPlaceable = + measurables + .fastFirstOrNull { it.layoutId == LayoutId.Media } + ?.measure(applyMediaConstraints(constraintsWithCutout, isMediaInRow)) + val quickSettingsPlaceable = + measurables + .fastFirst { it.layoutId == LayoutId.QuickSettings } + .measure(constraintsWithCutout) + val notificationsPlaceable = + measurables.fastFirst { it.layoutId == LayoutId.Notifications }.measure(constraints) + + val notificationsTop = + calculateNotificationsTop( + statusBarHeaderPlaceable = shadeHeaderPlaceable, + quickSettingsPlaceable = quickSettingsPlaceable, + mediaPlaceable = mediaPlaceable, + insetsTop = insetsTop, + isMediaInRow = isMediaInRow, + ) + onNotificationsTopChanged(notificationsTop) + + return layout(constraints.maxWidth, constraints.maxHeight) { + shadeHeaderPlaceable.placeRelative(x = insetsLeft, y = insetsTop) + quickSettingsPlaceable.placeRelative( + x = insetsLeft, + y = insetsTop + shadeHeaderPlaceable.height, + ) + + if (isMediaInRow) { + mediaPlaceable?.placeRelative( + x = insetsLeft + constraintsWithCutout.maxWidth / 2, + y = mediaOffset() + insetsTop + shadeHeaderPlaceable.height, + zIndex = mediaZIndex(), + ) + } else { + mediaPlaceable?.placeRelative( + x = insetsLeft, + y = insetsTop + shadeHeaderPlaceable.height + quickSettingsPlaceable.height, + zIndex = mediaZIndex(), + ) + } + + // Notifications don't need to accommodate for horizontal insets + notificationsPlaceable.placeRelative(x = 0, y = notificationsTop) + } + } + + private fun calculateNotificationsTop( + statusBarHeaderPlaceable: Placeable, + quickSettingsPlaceable: Placeable, + mediaPlaceable: Placeable?, + insetsTop: Int, + isMediaInRow: Boolean, + ): Int { + val mediaHeight = mediaPlaceable?.height ?: 0 + return insetsTop + + statusBarHeaderPlaceable.height + + if (isMediaInRow) { + max(quickSettingsPlaceable.height, mediaHeight) + } else { + quickSettingsPlaceable.height + mediaHeight + } + } + + private fun applyMediaConstraints( + constraints: Constraints, + isMediaInRow: Boolean, + ): Constraints { + return if (isMediaInRow) { + constraints.copy(maxWidth = constraints.maxWidth / 2) + } else { + constraints + } + } + + private fun MeasureScope.applyCutout( + constraints: Constraints, + cutoutInsets: WindowInsets?, + ): Constraints { + return if (cutoutInsets == null) { + constraints + } else { + val left = cutoutInsets.getLeft(this, layoutDirection) + val top = cutoutInsets.getTop(this) + val right = cutoutInsets.getRight(this, layoutDirection) + val bottom = cutoutInsets.getBottom(this) + + constraints.offset(horizontal = -(left + right), vertical = -(top + bottom)) + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt index 1db96cfd7c53..c9b801319b7f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt @@ -92,7 +92,12 @@ constructor( true } }, - color = MaterialTheme.colorScheme.surface, + color = + if (enabled) { + MaterialTheme.colorScheme.surface + } else { + MaterialTheme.colorScheme.surfaceContainerHighest + }, shape = RoundedCornerShape(28.dp), onClick = if (enabled) { @@ -119,7 +124,7 @@ constructor( modifier = Modifier.basicMarquee(), text = connectedDeviceViewModel.label.toString(), style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = connectedDeviceViewModel.labelColor.toColor(), maxLines = 1, ) connectedDeviceViewModel.deviceName?.let { @@ -127,7 +132,7 @@ constructor( modifier = Modifier.basicMarquee(), text = it.toString(), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, + color = connectedDeviceViewModel.deviceNameColor.toColor(), maxLines = 1, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index 072e91a25444..d4f3b5b6d6a6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -23,7 +23,6 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -78,7 +77,6 @@ fun ColumnVolumeSliders( modifier: Modifier = Modifier, ) { require(viewModels.isNotEmpty()) - val transition = updateTransition(isExpanded, label = "CollapsableSliders") Column(modifier = modifier) { Box( modifier = Modifier.fillMaxWidth(), @@ -106,8 +104,9 @@ fun ColumnVolumeSliders( sliderColors = sliderColors, ) } - transition.AnimatedVisibility( - visible = { it || !isExpandable }, + AnimatedVisibility( + visible = isExpanded || !isExpandable, + label = "CollapsableSliders", enter = expandVertically(animationSpec = tween(durationMillis = EXPAND_DURATION_MILLIS)), exit = @@ -120,23 +119,31 @@ fun ColumnVolumeSliders( for (index in 1..viewModels.lastIndex) { val sliderViewModel: SliderViewModel = viewModels[index] val sliderState by sliderViewModel.slider.collectAsStateWithLifecycle() - transition.AnimatedVisibility( - modifier = Modifier.padding(top = 16.dp), - visible = { it || !isExpandable }, - enter = enterTransition(index = index, totalCount = viewModels.size), - exit = exitTransition(index = index, totalCount = viewModels.size) - ) { - VolumeSlider( - modifier = Modifier.fillMaxWidth(), - state = sliderState, - onValueChange = { newValue: Float -> - sliderViewModel.onValueChanged(sliderState, newValue) - }, - onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, - onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, - sliderColors = sliderColors, - ) - } + + VolumeSlider( + modifier = + Modifier.padding(top = 16.dp) + .fillMaxWidth() + .animateEnterExit( + enter = + enterTransition( + index = index, + totalCount = viewModels.size, + ), + exit = + exitTransition( + index = index, + totalCount = viewModels.size, + ), + ), + state = sliderState, + onValueChange = { newValue: Float -> + sliderViewModel.onValueChanged(sliderState, newValue) + }, + onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, + onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, + sliderColors = sliderColors, + ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt index 5ffb6f82fbba..1cc0fb2aad9b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -25,13 +25,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.theme.PlatformTheme import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.res.R import com.android.systemui.volume.panel.ui.layout.ComponentsLayout @@ -43,7 +43,6 @@ private const val VolumePanelTestTag = "VolumePanel" private val padding = 24.dp @Composable -@OptIn(ExperimentalComposeUiApi::class) fun VolumePanelRoot( viewModel: VolumePanelViewModel, modifier: Modifier = Modifier, @@ -54,18 +53,20 @@ fun VolumePanelRoot( with(VolumePanelComposeScope(state)) { components?.let { componentsState -> - Components( - componentsState, - modifier - .sysuiResTag(VolumePanelTestTag) - .semantics { paneTitle = accessibilityTitle } - .padding( - start = padding, - top = padding, - end = padding, - bottom = 20.dp, - ) - ) + PlatformTheme { + Components( + componentsState, + modifier + .sysuiResTag(VolumePanelTestTag) + .semantics { paneTitle = accessibilityTitle } + .padding( + start = padding, + top = padding, + end = padding, + bottom = 20.dp, + ) + ) + } } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt index b16673702b49..d876606154fd 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt @@ -21,9 +21,7 @@ import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.SpringSpec import com.android.compose.animation.scene.content.state.TransitionState import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job -import kotlinx.coroutines.launch internal fun CoroutineScope.animateContent( layoutState: MutableSceneTransitionLayoutStateImpl, @@ -31,37 +29,24 @@ internal fun CoroutineScope.animateContent( oneOffAnimation: OneOffAnimation, targetProgress: Float, chain: Boolean = true, -) { - // Start the transition. This will compute the TransformationSpec associated to [transition], - // which we need to initialize the Animatable that will actually animate it. - layoutState.startTransition(transition, chain) +): Job { + oneOffAnimation.onRun = { + // Animate the progress to its target value. + val animationSpec = transition.transformationSpec.progressSpec + val visibilityThreshold = + (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold + val replacedTransition = transition.replacedTransition + val initialProgress = replacedTransition?.progress ?: 0f + val initialVelocity = replacedTransition?.progressVelocity ?: 0f + val animatable = + Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also { + oneOffAnimation.animatable = it + } - // The transition now contains the transformation spec that we should use to instantiate the - // Animatable. - val animationSpec = transition.transformationSpec.progressSpec - val visibilityThreshold = - (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold - val replacedTransition = transition.replacedTransition - val initialProgress = replacedTransition?.progress ?: 0f - val initialVelocity = replacedTransition?.progressVelocity ?: 0f - val animatable = - Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also { - oneOffAnimation.animatable = it - } + animatable.animateTo(targetProgress, animationSpec, initialVelocity) + } - // Animate the progress to its target value. - // - // Important: We start atomically to make sure that we start the coroutine even if it is - // cancelled right after it is launched, so that finishTransition() is correctly called. - // Otherwise, this transition will never be stopped and we will never settle to Idle. - oneOffAnimation.job = - launch(start = CoroutineStart.ATOMIC) { - try { - animatable.animateTo(targetProgress, animationSpec, initialVelocity) - } finally { - layoutState.finishTransition(transition) - } - } + return layoutState.startTransitionImmediately(animationScope = this, transition, chain) } internal class OneOffAnimation { @@ -74,8 +59,8 @@ internal class OneOffAnimation { */ lateinit var animatable: Animatable<Float, AnimationVector1D> - /** The job that is animating [animatable]. */ - lateinit var job: Job + /** The runnable to run for this animation. */ + lateinit var onRun: suspend () -> Unit val progress: Float get() = animatable.value @@ -83,7 +68,13 @@ internal class OneOffAnimation { val progressVelocity: Float get() = animatable.velocity - fun finish(): Job = job + suspend fun run() { + onRun() + } + + fun freezeAndAnimateToCurrentState() { + // Do nothing, the state of one-off animations never change and we directly animate to it. + } } // TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt index e020f14a9a02..28116cb435e4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateOverlay.kt @@ -18,7 +18,6 @@ package com.android.compose.animation.scene import com.android.compose.animation.scene.content.state.TransitionState import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job /** Trigger a one-off transition to show or hide an overlay. */ internal fun CoroutineScope.showOrHideOverlay( @@ -120,7 +119,13 @@ private class OneOffShowOrHideOverlayTransition( override val isInitiatedByUserInput: Boolean = false override val isUserInputOngoing: Boolean = false - override fun finish(): Job = oneOffAnimation.finish() + override suspend fun run() { + oneOffAnimation.run() + } + + override fun freezeAndAnimateToCurrentState() { + oneOffAnimation.freezeAndAnimateToCurrentState() + } } private class OneOffOverlayReplacingTransition( @@ -140,5 +145,11 @@ private class OneOffOverlayReplacingTransition( override val isInitiatedByUserInput: Boolean = false override val isUserInputOngoing: Boolean = false - override fun finish(): Job = oneOffAnimation.finish() + override suspend fun run() { + oneOffAnimation.run() + } + + override fun freezeAndAnimateToCurrentState() { + oneOffAnimation.freezeAndAnimateToCurrentState() + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index e15bc1243dd9..86be4a44f3cb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -28,7 +28,7 @@ internal fun CoroutineScope.animateToScene( layoutState: MutableSceneTransitionLayoutStateImpl, target: SceneKey, transitionKey: TransitionKey?, -): TransitionState.Transition.ChangeScene? { +): Pair<TransitionState.Transition.ChangeScene, Job>? { val transitionState = layoutState.transitionState if (transitionState.currentScene == target) { // This can happen in 3 different situations, for which there isn't anything else to do: @@ -139,7 +139,7 @@ private fun CoroutineScope.animateToScene( reversed: Boolean = false, fromScene: SceneKey = layoutState.transitionState.currentScene, chain: Boolean = true, -): TransitionState.Transition.ChangeScene { +): Pair<TransitionState.Transition.ChangeScene, Job> { val oneOffAnimation = OneOffAnimation() val targetProgress = if (reversed) 0f else 1f val transition = @@ -165,15 +165,16 @@ private fun CoroutineScope.animateToScene( ) } - animateContent( - layoutState = layoutState, - transition = transition, - oneOffAnimation = oneOffAnimation, - targetProgress = targetProgress, - chain = chain, - ) + val job = + animateContent( + layoutState = layoutState, + transition = transition, + oneOffAnimation = oneOffAnimation, + targetProgress = targetProgress, + chain = chain, + ) - return transition + return transition to job } private class OneOffSceneTransition( @@ -193,5 +194,11 @@ private class OneOffSceneTransition( override val isUserInputOngoing: Boolean = false - override fun finish(): Job = oneOffAnimation.finish() + override suspend fun run() { + oneOffAnimation.run() + } + + override fun freezeAndAnimateToCurrentState() { + oneOffAnimation.freezeAndAnimateToCurrentState() + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 37e4daafdc7b..24fef711d397 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -28,7 +28,6 @@ import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified import com.android.compose.nestedscroll.PriorityNestedScrollConnection import kotlin.math.absoluteValue -import kotlinx.coroutines.CoroutineScope internal interface DraggableHandler { /** @@ -63,7 +62,6 @@ internal interface DragController { internal class DraggableHandlerImpl( internal val layoutImpl: SceneTransitionLayoutImpl, internal val orientation: Orientation, - internal val coroutineScope: CoroutineScope, ) : DraggableHandler { internal val nestedScrollKey = Any() /** The [DraggableHandler] can only have one active [DragController] at a time. */ @@ -101,11 +99,6 @@ internal class DraggableHandlerImpl( val swipeAnimation = dragController.swipeAnimation - // Don't intercept a transition that is finishing. - if (swipeAnimation.isFinishing) { - return false - } - // Only intercept the current transition if one of the 2 swipes results is also a transition // between the same pair of contents. val swipes = computeSwipes(startedPosition, pointersDown = 1) @@ -140,7 +133,6 @@ internal class DraggableHandlerImpl( // This [transition] was already driving the animation: simply take over it. // Stop animating and start from the current offset. val oldSwipeAnimation = oldDragController.swipeAnimation - oldSwipeAnimation.cancelOffsetAnimation() // We need to recompute the swipe results since this is a new gesture, and the // fromScene.userActions may have changed. @@ -192,13 +184,7 @@ internal class DraggableHandlerImpl( else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)") } - return createSwipeAnimation( - layoutImpl, - layoutImpl.coroutineScope, - result, - isUpOrLeft, - orientation - ) + return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation) } private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes { @@ -279,16 +265,14 @@ private class DragControllerImpl( fun updateTransition(newTransition: SwipeAnimation<*>, force: Boolean = false) { if (force || isDrivingTransition) { - layoutState.startTransition(newTransition.contentTransition) + layoutState.startTransitionImmediately( + animationScope = draggableHandler.layoutImpl.animationScope, + newTransition.contentTransition, + true + ) } - val previous = swipeAnimation swipeAnimation = newTransition - - // Finish the previous transition. - if (previous != newTransition) { - layoutState.finishTransition(previous.contentTransition) - } } /** @@ -302,7 +286,7 @@ private class DragControllerImpl( } private fun <T : ContentKey> onDrag(delta: Float, swipeAnimation: SwipeAnimation<T>): Float { - if (delta == 0f || !isDrivingTransition || swipeAnimation.isFinishing) { + if (delta == 0f || !isDrivingTransition || swipeAnimation.isAnimatingOffset()) { return 0f } @@ -409,7 +393,7 @@ private class DragControllerImpl( swipeAnimation: SwipeAnimation<T>, ): Float { // The state was changed since the drag started; don't do anything. - if (!isDrivingTransition || swipeAnimation.isFinishing) { + if (!isDrivingTransition || swipeAnimation.isAnimatingOffset()) { return 0f } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index fd4c3100aa8d..5780c08950fa 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.util.fastSumBy import com.android.compose.ui.util.SpaceVectorConverter import kotlin.coroutines.cancellation.CancellationException import kotlin.math.sign -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -143,8 +143,8 @@ internal class MultiPointerDraggableNode( CompositionLocalConsumerModifierNode, ObserverModifierNode, SpaceVectorConverter { - private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() } - private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) + private val pointerTracker = delegate(SuspendingPointerInputModifierNode { pointerTracker() }) + private val pointerInput = delegate(SuspendingPointerInputModifierNode { pointerInput() }) private val velocityTracker = VelocityTracker() private var previousEnabled: Boolean = false @@ -153,7 +153,7 @@ internal class MultiPointerDraggableNode( // Reset the pointer input whenever enabled changed. if (value != field) { field = value - delegate.resetPointerInputHandler() + pointerInput.resetPointerInputHandler() } } @@ -173,7 +173,7 @@ internal class MultiPointerDraggableNode( if (value != field) { field = value converter = SpaceVectorConverter(value) - delegate.resetPointerInputHandler() + pointerInput.resetPointerInputHandler() } } @@ -186,19 +186,26 @@ internal class MultiPointerDraggableNode( observeReads { val newEnabled = enabled() if (newEnabled != previousEnabled) { - delegate.resetPointerInputHandler() + pointerInput.resetPointerInputHandler() } previousEnabled = newEnabled } } - override fun onCancelPointerInput() = delegate.onCancelPointerInput() + override fun onCancelPointerInput() { + pointerTracker.onCancelPointerInput() + pointerInput.onCancelPointerInput() + } override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize - ) = delegate.onPointerEvent(pointerEvent, pass, bounds) + ) { + // The order is important here: the tracker is always called first. + pointerTracker.onPointerEvent(pointerEvent, pass, bounds) + pointerInput.onPointerEvent(pointerEvent, pass, bounds) + } private var startedPosition: Offset? = null private var pointersDown: Int = 0 @@ -211,81 +218,77 @@ internal class MultiPointerDraggableNode( ) } + private suspend fun PointerInputScope.pointerTracker() { + val currentContext = currentCoroutineContext() + awaitPointerEventScope { + // Intercepts pointer inputs and exposes [PointersInfo], via + // [requireAncestorPointersInfoOwner], to our descendants. + while (currentContext.isActive) { + // During the Initial pass, we receive the event after our ancestors. + val pointers = awaitPointerEvent(PointerEventPass.Initial).changes + pointersDown = pointers.countDown() + if (pointersDown == 0) { + // There are no more pointers down + startedPosition = null + } else if (startedPosition == null) { + startedPosition = pointers.first().position + if (enabled()) { + onFirstPointerDown() + } + } + } + } + } + private suspend fun PointerInputScope.pointerInput() { if (!enabled()) { return } - coroutineScope { - launch { - // Intercepts pointer inputs and exposes [PointersInfo], via - // [requireAncestorPointersInfoOwner], to our descendants. - awaitPointerEventScope { - while (isActive) { - // During the Initial pass, we receive the event after our ancestors. - val pointers = awaitPointerEvent(PointerEventPass.Initial).changes - - pointersDown = pointers.countDown() - if (pointersDown == 0) { - // There are no more pointers down - startedPosition = null - } else if (startedPosition == null) { - startedPosition = pointers.first().position - onFirstPointerDown() - } - } - } - } - - // The order is important here: we want to make sure that the previous PointerEventScope - // is initialized first. This ensures that the following PointerEventScope doesn't - // receive more events than the first one. - launch { - awaitPointerEventScope { - while (isActive) { - try { - detectDragGestures( - orientation = orientation, - startDragImmediately = startDragImmediately, - onDragStart = { startedPosition, overSlop, pointersDown -> - velocityTracker.resetTracking() - onDragStarted(startedPosition, overSlop, pointersDown) - }, - onDrag = { controller, change, amount -> - velocityTracker.addPointerInputChange(change) - dispatchScrollEvents( - availableOnPreScroll = amount, - onScroll = { controller.onDrag(it) }, - source = NestedScrollSource.UserInput, - ) - }, - onDragEnd = { controller -> - startFlingGesture( - initialVelocity = - currentValueOf(LocalViewConfiguration) - .maximumFlingVelocity - .let { - val maxVelocity = Velocity(it, it) - velocityTracker.calculateVelocity(maxVelocity) - } - .toFloat(), - onFling = { controller.onStop(it, canChangeContent = true) } - ) - }, - onDragCancel = { controller -> - startFlingGesture( - initialVelocity = 0f, - onFling = { controller.onStop(it, canChangeContent = true) } - ) - }, - swipeDetector = swipeDetector, + val currentContext = currentCoroutineContext() + awaitPointerEventScope { + while (currentContext.isActive) { + try { + detectDragGestures( + orientation = orientation, + startDragImmediately = startDragImmediately, + onDragStart = { startedPosition, overSlop, pointersDown -> + velocityTracker.resetTracking() + onDragStarted(startedPosition, overSlop, pointersDown) + }, + onDrag = { controller, change, amount -> + velocityTracker.addPointerInputChange(change) + dispatchScrollEvents( + availableOnPreScroll = amount, + onScroll = { controller.onDrag(it) }, + source = NestedScrollSource.UserInput, + ) + }, + onDragEnd = { controller -> + startFlingGesture( + initialVelocity = + currentValueOf(LocalViewConfiguration) + .maximumFlingVelocity + .let { + val maxVelocity = Velocity(it, it) + velocityTracker.calculateVelocity(maxVelocity) + } + .toFloat(), + onFling = { controller.onStop(it, canChangeContent = true) } + ) + }, + onDragCancel = { controller -> + startFlingGesture( + initialVelocity = 0f, + onFling = { controller.onStop(it, canChangeContent = true) } ) - } catch (exception: CancellationException) { - // If the coroutine scope is active, we can just restart the drag cycle. - if (!isActive) { - throw exception - } - } + }, + swipeDetector = swipeDetector, + ) + } catch (exception: CancellationException) { + // If the coroutine scope is active, we can just restart the drag cycle. + if (!currentContext.isActive) { + throw exception } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt index 3a7c2bf5d331..bd21a69e0699 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt @@ -31,9 +31,6 @@ import kotlinx.coroutines.flow.flowOf * layout or Compose drawing phases. * 2. [ObservableTransitionState] values are backed by Kotlin [Flow]s and can be collected by * non-Compose code to observe value changes. - * 3. [ObservableTransitionState.Transition.fromScene] and - * [ObservableTransitionState.Transition.toScene] will never be equal, while - * [TransitionState.Transition.fromScene] and [TransitionState.Transition.toScene] can be equal. */ sealed interface ObservableTransitionState { /** @@ -54,9 +51,8 @@ sealed interface ObservableTransitionState { /** There is a transition animating between two scenes. */ sealed class Transition( - // TODO(b/353679003): Rename these to fromContent and toContent. - open val fromScene: ContentKey, - open val toScene: ContentKey, + val fromContent: ContentKey, + val toContent: ContentKey, val currentOverlays: Flow<Set<OverlayKey>>, val progress: Flow<Float>, @@ -86,8 +82,8 @@ sealed interface ObservableTransitionState { ) : ObservableTransitionState { override fun toString(): String = """Transition - |(from=$fromScene, - | to=$toScene, + |(from=$fromContent, + | to=$toContent, | isInitiatedByUserInput=$isInitiatedByUserInput, | isUserInputOngoing=$isUserInputOngoing |)""" @@ -95,8 +91,8 @@ sealed interface ObservableTransitionState { /** A transition animating between [fromScene] and [toScene]. */ class ChangeScene( - override val fromScene: SceneKey, - override val toScene: SceneKey, + val fromScene: SceneKey, + val toScene: SceneKey, val currentScene: Flow<SceneKey>, currentOverlays: Flow<Set<OverlayKey>>, progress: Flow<Float>, @@ -196,8 +192,8 @@ sealed interface ObservableTransitionState { fun isTransitioning(from: ContentKey? = null, to: ContentKey? = null): Boolean { return this is Transition && - (from == null || this.fromScene == from) && - (to == null || this.toScene == to) + (from == null || this.fromContent == from) && + (to == null || this.toContent == to) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index c2fb8fce1d6e..8480d3afaed4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -18,12 +18,17 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler -import androidx.compose.animation.core.spring +import androidx.compose.animation.core.snap import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable -import kotlin.coroutines.cancellation.CancellationException +import com.android.compose.animation.scene.UserActionResult.ChangeScene +import com.android.compose.animation.scene.UserActionResult.HideOverlay +import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay +import com.android.compose.animation.scene.UserActionResult.ShowOverlay +import com.android.compose.animation.scene.transition.animateProgress import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map @Composable internal fun PredictiveBackHandler( @@ -32,20 +37,21 @@ internal fun PredictiveBackHandler( ) { PredictiveBackHandler( enabled = result != null, - ) { progress: Flow<BackEventCompat> -> + ) { events: Flow<BackEventCompat> -> if (result == null) { // Note: We have to collect progress otherwise PredictiveBackHandler will throw. - progress.first() + events.first() return@PredictiveBackHandler } val animation = createSwipeAnimation( layoutImpl, - layoutImpl.coroutineScope, - result.userActionCopy( - transitionKey = result.transitionKey ?: TransitionKey.PredictiveBack - ), + if (result.transitionKey != null) { + result + } else { + result.copy(transitionKey = TransitionKey.PredictiveBack) + }, isUpOrLeft = false, // Note that the orientation does not matter here given that it's only used to // compute the distance. In our case the distance is always 1f. @@ -53,42 +59,30 @@ internal fun PredictiveBackHandler( distance = 1f, ) - animate(layoutImpl, animation, progress) - } -} - -private suspend fun <T : ContentKey> animate( - layoutImpl: SceneTransitionLayoutImpl, - animation: SwipeAnimation<T>, - progress: Flow<BackEventCompat>, -) { - fun animateOffset(targetContent: T) { - if ( - layoutImpl.state.transitionState != animation.contentTransition || animation.isFinishing - ) { - return - } + animateProgress( + state = layoutImpl.state, + animation = animation, + progress = events.map { it.progress }, - animation.animateOffset( - initialVelocity = 0f, - targetContent = targetContent, + // Use the transformationSpec.progressSpec. We will lazily access it later once the + // transition has been started, because at this point the transformation spec of the + // transition is not computed yet. + commitSpec = null, - // TODO(b/350705972): Allow to customize or reuse the same customization endpoints as - // the normal swipe transitions. We can't just reuse them here because other swipe - // transitions animate pixels while this transition animates progress, so the visibility - // thresholds will be completely different. - spec = spring(), + // The predictive back APIs will automatically animate the progress for us in this case + // so there is no need to animate it. + cancelSpec = snap(), ) } +} - layoutImpl.state.startTransition(animation.contentTransition) - try { - progress.collect { backEvent -> animation.dragOffset = backEvent.progress } - - // Back gesture successful. - animateOffset(animation.toContent) - } catch (e: CancellationException) { - // Back gesture cancelled. - animateOffset(animation.fromContent) +private fun UserActionResult.copy( + transitionKey: TransitionKey? = this.transitionKey +): UserActionResult { + return when (this) { + is ChangeScene -> copy(transitionKey = transitionKey) + is ShowOverlay -> copy(transitionKey = transitionKey) + is HideOverlay -> copy(transitionKey = transitionKey) + is ReplaceByOverlay -> copy(transitionKey = transitionKey) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index e4534302267a..094f20e337d6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -492,17 +492,6 @@ sealed class UserActionResult( ) { internal abstract fun toContent(currentScene: SceneKey): ContentKey - internal fun userActionCopy( - transitionKey: TransitionKey? = this.transitionKey - ): UserActionResult { - return when (this) { - is ChangeScene -> copy(transitionKey = transitionKey) - is ShowOverlay -> copy(transitionKey = transitionKey) - is HideOverlay -> copy(transitionKey = transitionKey) - is ReplaceByOverlay -> copy(transitionKey = transitionKey) - } - } - data class ChangeScene internal constructor( /** The scene we should be transitioning to during the [UserAction]. */ @@ -608,7 +597,7 @@ internal fun SceneTransitionLayoutForTesting( ) { val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current - val coroutineScope = rememberCoroutineScope() + val animationScope = rememberCoroutineScope() val layoutImpl = remember { SceneTransitionLayoutImpl( state = state as MutableSceneTransitionLayoutStateImpl, @@ -617,7 +606,7 @@ internal fun SceneTransitionLayoutForTesting( swipeSourceDetector = swipeSourceDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = builder, - coroutineScope = coroutineScope, + animationScope = animationScope, ) .also { onLayoutImpl?.invoke(it) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index b33b4f6c5019..f36c0fa2d75c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -59,7 +59,13 @@ internal class SceneTransitionLayoutImpl( internal var swipeSourceDetector: SwipeSourceDetector, internal var transitionInterceptionThreshold: Float, builder: SceneTransitionLayoutScope.() -> Unit, - internal val coroutineScope: CoroutineScope, + + /** + * The scope that should be used by *animations started by this layout only*, i.e. animations + * triggered by gestures set up on this layout in [swipeToScene] or interruption decay + * animations. + */ + internal val animationScope: CoroutineScope, ) { /** * The map of [Scene]s. @@ -142,18 +148,10 @@ internal class SceneTransitionLayoutImpl( // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the // current scene (required for SwipeTransition). horizontalDraggableHandler = - DraggableHandlerImpl( - layoutImpl = this, - orientation = Orientation.Horizontal, - coroutineScope = coroutineScope, - ) + DraggableHandlerImpl(layoutImpl = this, orientation = Orientation.Horizontal) verticalDraggableHandler = - DraggableHandlerImpl( - layoutImpl = this, - orientation = Orientation.Vertical, - coroutineScope = coroutineScope, - ) + DraggableHandlerImpl(layoutImpl = this, orientation = Orientation.Vertical) // Make sure that the state is created on the same thread (most probably the main thread) // than this STLImpl. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index f3128f1bf5c7..c2d5dd053148 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -30,6 +30,10 @@ import com.android.compose.animation.scene.transition.link.LinkedTransition import com.android.compose.animation.scene.transition.link.StateLink import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch /** * The state of a [SceneTransitionLayout]. @@ -108,24 +112,24 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState * If [targetScene] is different than the [currentScene][TransitionState.currentScene] of * [transitionState], then this will animate to [targetScene]. The associated * [TransitionState.Transition] will be returned and will be set as the current - * [transitionState] of this [MutableSceneTransitionLayoutState]. + * [transitionState] of this [MutableSceneTransitionLayoutState]. The [Job] in which the + * transition runs will be returned, allowing you to easily [join][Job.join] or + * [cancel][Job.cancel] the animation. * * Note that because a non-null [TransitionState.Transition] is returned does not mean that the * transition will finish and that we will settle to [targetScene]. The returned transition * might still be interrupted, for instance by another call to [setTargetScene] or by a user * gesture. * - * If [this] [CoroutineScope] is cancelled during the transition and that the transition was - * still active, then the [transitionState] of this [MutableSceneTransitionLayoutState] will be - * set to `TransitionState.Idle(targetScene)`. - * - * TODO(b/318794193): Add APIs to await() and cancel() any [TransitionState.Transition]. + * If [animationScope] is cancelled during the transition and that the transition was still + * active, then the [transitionState] of this [MutableSceneTransitionLayoutState] will be set to + * `TransitionState.Idle(targetScene)`. */ fun setTargetScene( targetScene: SceneKey, - coroutineScope: CoroutineScope, + animationScope: CoroutineScope, transitionKey: TransitionKey? = null, - ): TransitionState.Transition? + ): Pair<TransitionState.Transition, Job>? /** Immediately snap to the given [scene]. */ fun snapToScene( @@ -297,12 +301,12 @@ internal class MutableSceneTransitionLayoutStateImpl( override fun setTargetScene( targetScene: SceneKey, - coroutineScope: CoroutineScope, + animationScope: CoroutineScope, transitionKey: TransitionKey?, - ): TransitionState.Transition.ChangeScene? { + ): Pair<TransitionState.Transition.ChangeScene, Job>? { checkThread() - return coroutineScope.animateToScene( + return animationScope.animateToScene( layoutState = this@MutableSceneTransitionLayoutStateImpl, target = targetScene, transitionKey = transitionKey, @@ -310,17 +314,67 @@ internal class MutableSceneTransitionLayoutStateImpl( } /** + * Instantly start a [transition], running it in [animationScope]. + * + * This call returns immediately and [transition] will be the [currentTransition] of this + * [MutableSceneTransitionLayoutState]. + * + * @see startTransition + */ + internal fun startTransitionImmediately( + animationScope: CoroutineScope, + transition: TransitionState.Transition, + chain: Boolean = true, + ): Job { + // Note that we start with UNDISPATCHED so that startTransition() is called directly and + // transition becomes the current [transitionState] right after this call. + return animationScope.launch( + start = CoroutineStart.UNDISPATCHED, + ) { + startTransition(transition, chain) + } + } + + /** * Start a new [transition]. * * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and * will run in parallel to the current transitions. If [chain] is `false`, then the list of * [currentTransitions] will be cleared and [transition] will be the only running transition. * - * Important: you *must* call [finishTransition] once the transition is finished. + * If any transition is currently ongoing, it will be interrupted and forced to animate to its + * current state. + * + * This method returns when [transition] is done running, i.e. when the call to + * [run][TransitionState.Transition.run] returns. */ - internal fun startTransition(transition: TransitionState.Transition, chain: Boolean = true) { + internal suspend fun startTransition( + transition: TransitionState.Transition, + chain: Boolean = true, + ) { checkThread() + try { + // Keep a reference to the previous transition (if any). + val previousTransition = currentTransition + + // Start the transition. + startTransitionInternal(transition, chain) + + // Handle transition links. + previousTransition?.let { cancelActiveTransitionLinks(it) } + if (stateLinks.isNotEmpty()) { + coroutineScope { setupTransitionLinks(transition) } + } + + // Run the transition until it is finished. + transition.run() + } finally { + finishTransition(transition) + } + } + + private fun startTransitionInternal(transition: TransitionState.Transition, chain: Boolean) { // Set the current scene and overlays on the transition. val currentState = transitionState transition.currentSceneWhenTransitionStarted = currentState.currentScene @@ -349,10 +403,6 @@ internal class MutableSceneTransitionLayoutStateImpl( transition.updateOverscrollSpecs(fromSpec = null, toSpec = null) } - // Handle transition links. - currentTransition?.let { cancelActiveTransitionLinks(it) } - setupTransitionLinks(transition) - if (!enableInterruptions) { // Set the current transition. check(transitionStates.size == 1) @@ -367,9 +417,8 @@ internal class MutableSceneTransitionLayoutStateImpl( transitionStates = listOf(transition) } is TransitionState.Transition -> { - // Force the current transition to finish to currentScene. The transition will call - // [finishTransition] once it's finished. - currentState.finish() + // Force the current transition to finish to currentScene. + currentState.freezeAndAnimateToCurrentState() val tooManyTransitions = transitionStates.size >= MAX_CONCURRENT_TRANSITIONS val clearCurrentTransitions = !chain || tooManyTransitions @@ -423,7 +472,7 @@ internal class MutableSceneTransitionLayoutStateImpl( transition.activeTransitionLinks.clear() } - private fun setupTransitionLinks(transition: TransitionState.Transition) { + private fun CoroutineScope.setupTransitionLinks(transition: TransitionState.Transition) { stateLinks.fastForEach { stateLink -> val matchingLinks = stateLink.transitionLinks.fastFilter { it.isMatchingLink(transition) } @@ -443,7 +492,11 @@ internal class MutableSceneTransitionLayoutStateImpl( key = matchingLink.targetTransitionKey, ) - stateLink.target.startTransition(linkedTransition) + // Start with UNDISPATCHED so that startTransition is called directly and the new linked + // transition is observable directly. + launch(start = CoroutineStart.UNDISPATCHED) { + stateLink.target.startTransition(linkedTransition) + } transition.activeTransitionLinks[stateLink] = linkedTransition } } @@ -453,7 +506,7 @@ internal class MutableSceneTransitionLayoutStateImpl( * [currentScene][TransitionState.currentScene]. This will do nothing if [transition] was * interrupted since it was started. */ - internal fun finishTransition(transition: TransitionState.Transition) { + private fun finishTransition(transition: TransitionState.Transition) { checkThread() if (finishedTransitions.contains(transition)) { @@ -461,6 +514,10 @@ internal class MutableSceneTransitionLayoutStateImpl( return } + // Make sure that this transition settles in case it was force finished, for instance by + // calling snapToScene(). + transition.freezeAndAnimateToCurrentState() + val transitionStates = this.transitionStates if (!transitionStates.contains(transition)) { // This transition was already removed from transitionStates. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index 57ff597d7314..2a09a77788e7 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -17,8 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -28,14 +28,10 @@ import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified import kotlin.math.absoluteValue -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.CompletableDeferred internal fun createSwipeAnimation( layoutState: MutableSceneTransitionLayoutStateImpl, - animationScope: CoroutineScope, result: UserActionResult, isUpOrLeft: Boolean, orientation: Orientation, @@ -43,7 +39,6 @@ internal fun createSwipeAnimation( ): SwipeAnimation<*> { return createSwipeAnimation( layoutState, - animationScope, result, isUpOrLeft, orientation, @@ -56,7 +51,6 @@ internal fun createSwipeAnimation( internal fun createSwipeAnimation( layoutImpl: SceneTransitionLayoutImpl, - animationScope: CoroutineScope, result: UserActionResult, isUpOrLeft: Boolean, orientation: Orientation, @@ -88,7 +82,6 @@ internal fun createSwipeAnimation( return createSwipeAnimation( layoutImpl.state, - animationScope, result, isUpOrLeft, orientation, @@ -99,7 +92,6 @@ internal fun createSwipeAnimation( private fun createSwipeAnimation( layoutState: MutableSceneTransitionLayoutStateImpl, - animationScope: CoroutineScope, result: UserActionResult, isUpOrLeft: Boolean, orientation: Orientation, @@ -109,7 +101,6 @@ private fun createSwipeAnimation( fun <T : ContentKey> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> { return SwipeAnimation( layoutState = layoutState, - animationScope = animationScope, fromContent = fromContent, toContent = toContent, orientation = orientation, @@ -197,7 +188,6 @@ internal fun createSwipeAnimation(old: SwipeAnimation<*>): SwipeAnimation<*> { /** A helper class that contains the main logic for swipe transitions. */ internal class SwipeAnimation<T : ContentKey>( val layoutState: MutableSceneTransitionLayoutStateImpl, - val animationScope: CoroutineScope, val fromContent: T, val toContent: T, override val orientation: Orientation, @@ -210,18 +200,26 @@ internal class SwipeAnimation<T : ContentKey>( /** The [TransitionState.Transition] whose implementation delegates to this [SwipeAnimation]. */ lateinit var contentTransition: TransitionState.Transition - var currentContent by mutableStateOf(currentContent) + private var _currentContent by mutableStateOf(currentContent) + var currentContent: T + get() = _currentContent + set(value) { + check(!isAnimatingOffset()) { + "currentContent can not be changed once we are animating the offset" + } + _currentContent = value + } val progress: Float get() { // Important: If we are going to return early because distance is equal to 0, we should // still make sure we read the offset before returning so that the calling code still // subscribes to the offset value. - val animatable = offsetAnimation?.animatable + val animatable = offsetAnimation val offset = when { + isInPreviewStage -> 0f animatable != null -> animatable.value - contentTransition.previewTransformationSpec != null -> 0f else -> dragOffset } @@ -238,7 +236,7 @@ internal class SwipeAnimation<T : ContentKey>( val progressVelocity: Float get() { - val animatable = offsetAnimation?.animatable ?: return 0f + val animatable = offsetAnimation ?: return 0f val distance = distance() if (distance == DistanceUnspecified) { return 0f @@ -249,7 +247,15 @@ internal class SwipeAnimation<T : ContentKey>( } val previewProgress: Float - get() = computeProgress(dragOffset) + get() { + val offset = + if (isInPreviewStage) { + offsetAnimation?.value ?: dragOffset + } else { + dragOffset + } + return computeProgress(offset) + } val previewProgressVelocity: Float get() = 0f @@ -263,7 +269,8 @@ internal class SwipeAnimation<T : ContentKey>( var dragOffset by mutableFloatStateOf(dragOffset) /** The offset animation that animates the offset once the user lifts their finger. */ - private var offsetAnimation: OffsetAnimation? by mutableStateOf(null) + private var offsetAnimation: Animatable<Float, AnimationVector1D>? by mutableStateOf(null) + private val offsetAnimationRunnable = CompletableDeferred<(suspend () -> Unit)?>() val isUserInputOngoing: Boolean get() = offsetAnimation == null @@ -271,15 +278,10 @@ internal class SwipeAnimation<T : ContentKey>( override val absoluteDistance: Float get() = distance().absoluteValue - /** Whether [finish] was called on this animation. */ - var isFinishing = false - private set - constructor( other: SwipeAnimation<T> ) : this( layoutState = other.layoutState, - animationScope = other.animationScope, fromContent = other.fromContent, toContent = other.toContent, orientation = other.orientation, @@ -287,9 +289,17 @@ internal class SwipeAnimation<T : ContentKey>( requiresFullDistanceSwipe = other.requiresFullDistanceSwipe, distance = other.distance, currentContent = other.currentContent, - dragOffset = other.dragOffset, + dragOffset = other.offsetAnimation?.value ?: other.dragOffset, ) + suspend fun run() { + // This animation will first be driven by finger, then when the user lift their finger we + // start an animation to the target offset (progress = 1f or progress = 0f). We await() for + // offsetAnimationRunnable to be completed and then run it. + val runAnimation = offsetAnimationRunnable.await() ?: return + runAnimation() + } + /** * The signed distance between [fromContent] and [toContent]. It is negative if [fromContent] is * above or to the left of [toContent]. @@ -300,28 +310,15 @@ internal class SwipeAnimation<T : ContentKey>( */ fun distance(): Float = distance(this) - /** Ends any previous [offsetAnimation] and runs the new [animation]. */ - private fun startOffsetAnimation(animation: () -> OffsetAnimation): OffsetAnimation { - cancelOffsetAnimation() - return animation().also { offsetAnimation = it } - } - - /** Cancel any ongoing offset animation. */ - // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at - // the same time. - fun cancelOffsetAnimation() { - val animation = offsetAnimation ?: return - offsetAnimation = null - - dragOffset = animation.animatable.value - animation.job.cancel() - } + fun isAnimatingOffset(): Boolean = offsetAnimation != null fun animateOffset( initialVelocity: Float, targetContent: T, - spec: SpringSpec<Float>? = null, - ): OffsetAnimation { + spec: AnimationSpec<Float>? = null, + ) { + check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" } + val initialProgress = progress // Skip the animation if we have already reached the target content and the overscroll does // not animate anything. @@ -358,74 +355,80 @@ internal class SwipeAnimation<T : ContentKey>( currentContent = targetContent } - return startOffsetAnimation { - val startProgress = - if (contentTransition.previewTransformationSpec != null) 0f else dragOffset - val animatable = Animatable(startProgress, OffsetVisibilityThreshold) - val isTargetGreater = targetOffset > animatable.value - val startedWhenOvercrollingTargetContent = - if (targetContent == fromContent) initialProgress < 0f else initialProgress > 1f - val job = - animationScope - // Important: We start atomically to make sure that we start the coroutine even - // if it is cancelled right after it is launched, so that snapToContent() is - // correctly called. Otherwise, this transition will never be stopped and we - // will never settle to Idle. - .launch(start = CoroutineStart.ATOMIC) { - // TODO(b/327249191): Refactor the code so that we don't even launch a - // coroutine if we don't need to animate. - if (skipAnimation) { - snapToContent(targetContent) - dragOffset = targetOffset - return@launch - } + val startProgress = + if (contentTransition.previewTransformationSpec != null && targetContent == toContent) { + 0f + } else { + dragOffset + } + + val animatable = + Animatable(startProgress, OffsetVisibilityThreshold).also { offsetAnimation = it } + + check(isAnimatingOffset()) + + // Note: we still create the animatable and set it on offsetAnimation even when + // skipAnimation is true, just so that isUserInputOngoing and isAnimatingOffset() are + // unchanged even despite this small skip-optimization (which is just an implementation + // detail). + if (skipAnimation) { + // Unblock the job. + offsetAnimationRunnable.complete(null) + return + } - try { - val swipeSpec = - spec - ?: contentTransition.transformationSpec.swipeSpec - ?: layoutState.transitions.defaultSwipeSpec - animatable.animateTo( - targetValue = targetOffset, - animationSpec = swipeSpec, - initialVelocity = initialVelocity, - ) { - if (bouncingContent == null) { - val isBouncing = - if (isTargetGreater) { - if (startedWhenOvercrollingTargetContent) { - value >= targetOffset - } else { - value > targetOffset - } - } else { - if (startedWhenOvercrollingTargetContent) { - value <= targetOffset - } else { - value < targetOffset - } - } - - if (isBouncing) { - bouncingContent = targetContent - - // Immediately stop this transition if we are bouncing on a - // content that does not bounce. - if (!contentTransition.isWithinProgressRange(progress)) { - snapToContent(targetContent) - } - } + val isTargetGreater = targetOffset > animatable.value + val startedWhenOvercrollingTargetContent = + if (targetContent == fromContent) initialProgress < 0f else initialProgress > 1f + + val swipeSpec = + spec + ?: contentTransition.transformationSpec.swipeSpec + ?: layoutState.transitions.defaultSwipeSpec + + offsetAnimationRunnable.complete { + try { + animatable.animateTo( + targetValue = targetOffset, + animationSpec = swipeSpec, + initialVelocity = initialVelocity, + ) { + if (bouncingContent == null) { + val isBouncing = + if (isTargetGreater) { + if (startedWhenOvercrollingTargetContent) { + value >= targetOffset + } else { + value > targetOffset } + } else { + if (startedWhenOvercrollingTargetContent) { + value <= targetOffset + } else { + value < targetOffset + } + } + + if (isBouncing) { + bouncingContent = targetContent + + // Immediately stop this transition if we are bouncing on a content that + // does not bounce. + if (!contentTransition.isWithinProgressRange(progress)) { + throw SnapException() } - } finally { - snapToContent(targetContent) } } - - OffsetAnimation(animatable, job) + } + } catch (_: SnapException) { + /* Ignore. */ + } } } + /** An exception thrown during the animation to stop it immediately. */ + private class SnapException : Exception() + private fun canChangeContent(targetContent: ContentKey): Boolean { return when (val transition = contentTransition) { is TransitionState.Transition.ChangeScene -> @@ -446,34 +449,11 @@ internal class SwipeAnimation<T : ContentKey>( } } - private fun snapToContent(content: T) { - cancelOffsetAnimation() - check(currentContent == content) - layoutState.finishTransition(contentTransition) - } - - fun finish(): Job { - if (isFinishing) return requireNotNull(offsetAnimation).job - isFinishing = true - - // If we were already animating the offset, simply return the job. - offsetAnimation?.let { - return it.job - } + fun freezeAndAnimateToCurrentState() { + if (isAnimatingOffset()) return - // Animate to the current content. - val animation = animateOffset(initialVelocity = 0f, targetContent = currentContent) - check(offsetAnimation == animation) - return animation.job + animateOffset(initialVelocity = 0f, targetContent = currentContent) } - - internal class OffsetAnimation( - /** The animatable used to animate the offset. */ - val animatable: Animatable<Float, AnimationVector1D>, - - /** The job in which [animatable] is animated. */ - val job: Job, - ) } private object DefaultSwipeDistance : UserActionDistance { @@ -537,7 +517,13 @@ private class ChangeSceneSwipeTransition( override val isUserInputOngoing: Boolean get() = swipeAnimation.isUserInputOngoing - override fun finish(): Job = swipeAnimation.finish() + override suspend fun run() { + swipeAnimation.run() + } + + override fun freezeAndAnimateToCurrentState() { + swipeAnimation.freezeAndAnimateToCurrentState() + } } private class ShowOrHideOverlaySwipeTransition( @@ -594,7 +580,13 @@ private class ShowOrHideOverlaySwipeTransition( override val isUserInputOngoing: Boolean get() = swipeAnimation.isUserInputOngoing - override fun finish(): Job = swipeAnimation.finish() + override suspend fun run() { + swipeAnimation.run() + } + + override fun freezeAndAnimateToCurrentState() { + swipeAnimation.freezeAndAnimateToCurrentState() + } } private class ReplaceOverlaySwipeTransition( @@ -645,5 +637,11 @@ private class ReplaceOverlaySwipeTransition( override val isUserInputOngoing: Boolean get() = swipeAnimation.isUserInputOngoing - override fun finish(): Job = swipeAnimation.finish() + override suspend fun run() { + swipeAnimation.run() + } + + override fun freezeAndAnimateToCurrentState() { + swipeAnimation.freezeAndAnimateToCurrentState() + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 2b5953c586db..1f82e0bd026a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -104,23 +104,23 @@ interface SceneTransitionsBuilder { ): TransitionSpec /** - * Define the animation to be played when the [scene] is overscrolled in the given + * Define the animation to be played when the [content] is overscrolled in the given * [orientation]. * * The overscroll animation always starts from a progress of 0f, and reaches 1f when moving the * [distance] down/right, -1f when moving in the opposite direction. */ fun overscroll( - scene: SceneKey, + content: ContentKey, orientation: Orientation, builder: OverscrollBuilder.() -> Unit, ): OverscrollSpec /** - * Prevents overscroll the [scene] in the given [orientation], allowing ancestors to eventually - * consume the remaining gesture. + * Prevents overscroll the [content] in the given [orientation], allowing ancestors to + * eventually consume the remaining gesture. */ - fun overscrollDisabled(scene: SceneKey, orientation: Orientation): OverscrollSpec + fun overscrollDisabled(content: ContentKey, orientation: Orientation): OverscrollSpec } interface BaseTransitionBuilder : PropertyTransformationBuilder { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 18e356f71768..da4c8d8db752 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -84,30 +84,30 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { } override fun overscroll( - scene: SceneKey, + content: ContentKey, orientation: Orientation, builder: OverscrollBuilder.() -> Unit ): OverscrollSpec { val impl = OverscrollBuilderImpl().apply(builder) check(impl.transformations.isNotEmpty()) { "This method does not allow empty transformations. " + - "Use overscrollDisabled($scene, $orientation) instead." + "Use overscrollDisabled($content, $orientation) instead." } - return overscrollSpec(scene, orientation, impl) + return overscrollSpec(content, orientation, impl) } - override fun overscrollDisabled(scene: SceneKey, orientation: Orientation): OverscrollSpec { - return overscrollSpec(scene, orientation, OverscrollBuilderImpl()) + override fun overscrollDisabled(content: ContentKey, orientation: Orientation): OverscrollSpec { + return overscrollSpec(content, orientation, OverscrollBuilderImpl()) } private fun overscrollSpec( - scene: SceneKey, + content: ContentKey, orientation: Orientation, impl: OverscrollBuilderImpl, ): OverscrollSpec { val spec = OverscrollSpecImpl( - content = scene, + content = content, orientation = orientation, transformationSpec = TransformationSpecImpl( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index 0cd8c1af0507..a47caaa15f18 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -35,7 +35,6 @@ import com.android.compose.animation.scene.TransformationSpecImpl import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.transition.link.LinkedTransition import com.android.compose.animation.scene.transition.link.StateLink -import kotlinx.coroutines.Job import kotlinx.coroutines.launch /** The state associated to a [SceneTransitionLayout] at some specific point in time. */ @@ -300,19 +299,19 @@ sealed interface TransitionState { return fromContent == content || toContent == content } + /** Run this transition and return once it is finished. */ + internal abstract suspend fun run() + /** - * Force this transition to finish and animate to an [Idle] state. - * - * Important: Once this is called, the effective state of the transition should remain - * unchanged. For instance, in the case of a [TransitionState.Transition], its - * [currentScene][TransitionState.Transition.currentScene] should never change once [finish] - * is called. + * Freeze this transition state so that neither [currentScene] nor [currentOverlays] will + * change in the future, and animate the progress towards that state. For instance, a + * [Transition.ChangeScene] should animate the progress to 0f if its [currentScene] is equal + * to its [fromScene][Transition.ChangeScene.fromScene] or animate it to 1f if its equal to + * its [toScene][Transition.ChangeScene.toScene]. * - * @return the [Job] that animates to the idle state. It can be used to wait until the - * animation is complete or cancel it to snap the animation. Calling [finish] multiple - * times will return the same [Job]. + * This is called when this transition is interrupted (replaced) by another transition. */ - internal abstract fun finish(): Job + internal abstract fun freezeAndAnimateToCurrentState() internal fun updateOverscrollSpecs( fromSpec: OverscrollSpecImpl?, @@ -350,7 +349,7 @@ sealed interface TransitionState { fun create(): Animatable<Float, AnimationVector1D> { val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) - layoutImpl.coroutineScope.launch { + layoutImpl.animationScope.launch { val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec val progressSpec = spring( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt new file mode 100644 index 000000000000..715d979e116e --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2024 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.compose.animation.scene.transition + +import androidx.annotation.FloatRange +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.util.fastCoerceIn +import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.MutableSceneTransitionLayoutState +import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateImpl +import com.android.compose.animation.scene.OverlayKey +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SwipeAnimation +import com.android.compose.animation.scene.TransitionKey +import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.createSwipeAnimation +import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * Seek to the given [scene] using [progress]. + * + * This will start a transition from the + * [current scene][MutableSceneTransitionLayoutState.currentScene] to [scene], driven by the + * progress in [progress]. Once [progress] stops emitting, we will animate progress to 1f (using + * [animationSpec]) if it stopped normally or to 0f if it stopped with a + * [kotlin.coroutines.cancellation.CancellationException]. + */ +suspend fun MutableSceneTransitionLayoutState.seekToScene( + scene: SceneKey, + @FloatRange(0.0, 1.0) progress: Flow<Float>, + transitionKey: TransitionKey? = null, + animationSpec: AnimationSpec<Float>? = null, +) { + require(scene != currentScene) { + "seekToScene($scene) has to be called with a different scene than the current scene" + } + + seek(UserActionResult.ChangeScene(scene, transitionKey), progress, animationSpec) +} + +/** + * Seek to show the given [overlay] using [progress]. + * + * This will start a transition to show [overlay] from the + * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in + * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using + * [animationSpec]) if it stopped normally or to 0f if it stopped with a + * [kotlin.coroutines.cancellation.CancellationException]. + */ +suspend fun MutableSceneTransitionLayoutState.seekToShowOverlay( + overlay: OverlayKey, + @FloatRange(0.0, 1.0) progress: Flow<Float>, + transitionKey: TransitionKey? = null, + animationSpec: AnimationSpec<Float>? = null, +) { + require(overlay in currentOverlays) { + "seekToShowOverlay($overlay) can be called only when the overlay is in currentOverlays" + } + + seek(UserActionResult.ShowOverlay(overlay, transitionKey), progress, animationSpec) +} + +/** + * Seek to hide the given [overlay] using [progress]. + * + * This will start a transition to hide [overlay] to the + * [current scene][MutableSceneTransitionLayoutState.currentScene], driven by the progress in + * [progress]. Once [progress] stops emitting, we will animate progress to 1f (using + * [animationSpec]) if it stopped normally or to 0f if it stopped with a + * [kotlin.coroutines.cancellation.CancellationException]. + */ +suspend fun MutableSceneTransitionLayoutState.seekToHideOverlay( + overlay: OverlayKey, + @FloatRange(0.0, 1.0) progress: Flow<Float>, + transitionKey: TransitionKey? = null, + animationSpec: AnimationSpec<Float>? = null, +) { + require(overlay !in currentOverlays) { + "seekToHideOverlay($overlay) can be called only when the overlay is *not* in " + + "currentOverlays" + } + + seek(UserActionResult.HideOverlay(overlay, transitionKey), progress, animationSpec) +} + +private suspend fun MutableSceneTransitionLayoutState.seek( + result: UserActionResult, + progress: Flow<Float>, + animationSpec: AnimationSpec<Float>?, +) { + val layoutState = + when (this) { + is MutableSceneTransitionLayoutStateImpl -> this + } + + val swipeAnimation = + createSwipeAnimation( + layoutState = layoutState, + result = result, + + // We are animating progress, so distance is always 1f. + distance = 1f, + + // The orientation and isUpOrLeft don't matter here given that they are only used during + // overscroll, which is disabled for progress-based transitions. + orientation = Orientation.Horizontal, + isUpOrLeft = false, + ) + + animateProgress( + state = layoutState, + animation = swipeAnimation, + progress = progress, + commitSpec = animationSpec, + cancelSpec = animationSpec, + ) +} + +internal suspend fun <T : ContentKey> animateProgress( + state: MutableSceneTransitionLayoutStateImpl, + animation: SwipeAnimation<T>, + progress: Flow<Float>, + commitSpec: AnimationSpec<Float>?, + cancelSpec: AnimationSpec<Float>?, +) { + fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { + if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) { + return + } + + animation.animateOffset( + initialVelocity = 0f, + targetContent = targetContent, + + // Important: we have to specify a spec that correctly animates *progress* (low + // visibility threshold) and not *offset* (higher visibility threshold). + spec = spec ?: animation.contentTransition.transformationSpec.progressSpec, + ) + } + + coroutineScope { + val collectionJob = launch { + try { + progress.collectLatest { progress -> + // Progress based animation should never overscroll given that the + // absoluteDistance exposed to overscroll builders is always 1f and will not + // lead to any noticeable transformation. + animation.dragOffset = progress.fastCoerceIn(0f, 1f) + } + + // Transition committed. + animateOffset(animation.toContent, commitSpec) + } catch (e: CancellationException) { + // Transition cancelled. + animateOffset(animation.fromContent, cancelSpec) + } + } + + // Start the transition. + state.startTransition(animation.contentTransition) + + // The transition is done. Cancel the collection in case the transition was finished because + // it was interrupted by another transition. + if (collectionJob.isActive) { + collectionJob.cancel() + } + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt index 564d4b3a3c5a..42ba9ba95e07 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt @@ -19,7 +19,6 @@ package com.android.compose.animation.scene.transition.link import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.content.state.TransitionState -import kotlinx.coroutines.Job /** A linked transition which is driven by a [originalTransition]. */ internal class LinkedTransition( @@ -50,5 +49,11 @@ internal class LinkedTransition( override val progressVelocity: Float get() = originalTransition.progressVelocity - override fun finish(): Job = originalTransition.finish() + override suspend fun run() { + originalTransition.run() + } + + override fun freezeAndAnimateToCurrentState() { + originalTransition.freezeAndAnimateToCurrentState() + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt index 8ebb42aa24f8..a491349ca757 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt @@ -39,7 +39,10 @@ import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.TestScenes.SceneD +import com.android.compose.test.setContentAndCreateMainScope +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Rule @@ -406,30 +409,33 @@ class AnimatedSharedAsStateTest { } } - rule.setContent { - SceneTransitionLayout(state) { - // foo goes from 0f to 100f in A => B. - scene(SceneA) { animateFloat(0f, foo) } - scene(SceneB) { animateFloat(100f, foo) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + // foo goes from 0f to 100f in A => B. + scene(SceneA) { animateFloat(0f, foo) } + scene(SceneB) { animateFloat(100f, foo) } - // bar goes from 0f to 10f in C => D. - scene(SceneC) { animateFloat(0f, bar) } - scene(SceneD) { animateFloat(10f, bar) } + // bar goes from 0f to 10f in C => D. + scene(SceneC) { animateFloat(0f, bar) } + scene(SceneD) { animateFloat(10f, bar) } + } } - } - rule.runOnUiThread { - // A => B is at 30%. + // A => B is at 30%. + scope.launch { state.startTransition( transition( from = SceneA, to = SceneB, progress = { 0.3f }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) ) + } - // C => D is at 70%. + // C => D is at 70%. + scope.launch { state.startTransition(transition(from = SceneC, to = SceneD, progress = { 0.7f })) } rule.waitForIdle() @@ -466,17 +472,18 @@ class AnimatedSharedAsStateTest { } } - rule.setContent { - SceneTransitionLayout(state) { - scene(SceneA) { animateFloat(0f, key) } - scene(SceneB) { animateFloat(100f, key) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneA) { animateFloat(0f, key) } + scene(SceneB) { animateFloat(100f, key) } + } } - } // Overscroll on A at -100%: value should be interpolated given that there is no overscroll // defined for scene A. var progress by mutableStateOf(-1f) - rule.runOnIdle { + scope.launch { state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress })) } rule.waitForIdle() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 9fa4722cf86f..79f82c948541 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -41,9 +41,9 @@ import com.android.compose.animation.scene.content.state.TransitionState.Transit import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.MonotonicClockTestScope import com.android.compose.test.runMonotonicClockTest +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import org.junit.Test import org.junit.runner.RunWith @@ -132,7 +132,10 @@ class DraggableHandlerTest { swipeSourceDetector = DefaultEdgeDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenesBuilder, - coroutineScope = testScope, + + // Use testScope and not backgroundScope here because backgroundScope does not + // work well with advanceUntilIdle(), which is used by some tests. + animationScope = testScope, ) .apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) } @@ -197,6 +200,8 @@ class DraggableHandlerTest { fromScene: SceneKey? = null, toScene: SceneKey? = null, progress: Float? = null, + previewProgress: Float? = null, + isInPreviewStage: Boolean? = null, isUserInputOngoing: Boolean? = null ): Transition { val transition = assertThat(transitionState).isSceneTransition() @@ -204,6 +209,10 @@ class DraggableHandlerTest { fromScene?.let { assertThat(transition).hasFromScene(it) } toScene?.let { assertThat(transition).hasToScene(it) } progress?.let { assertThat(transition).hasProgress(it) } + previewProgress?.let { assertThat(transition).hasPreviewProgress(it) } + isInPreviewStage?.let { + assertThat(transition).run { if (it) isInPreviewStage() else isNotInPreviewStage() } + } isUserInputOngoing?.let { assertThat(transition).hasIsUserInputOngoing(it) } return transition } @@ -301,8 +310,20 @@ class DraggableHandlerTest { runMonotonicClockTest { val testGestureScope = TestGestureScope(testScope = this) - // run the test - testGestureScope.block() + try { + // Run the test. + testGestureScope.block() + } finally { + // Make sure we stop the last transition if it was not explicitly stopped, otherwise + // tests will time out after 10s given that the transitions are now started on the + // test scope. We don't use backgroundScope when starting the test transitions + // because coroutines started on the background scope don't work well with + // advanceUntilIdle(), which is used in a few tests. + if (testGestureScope.draggableHandler.isDrivingTransition) { + (testGestureScope.layoutState.transitionState as Transition) + .freezeAndAnimateToCurrentState() + } + } } } @@ -338,6 +359,32 @@ class DraggableHandlerTest { } @Test + fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene_previewAnimated() = + runGestureTest { + layoutState.transitions = transitions { + // set a preview for the transition + from(SceneA, to = SceneC, preview = {}) {} + } + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + assertTransition(currentScene = SceneA) + + dragController.onDragStopped(velocity = velocityThreshold - 0.01f) + runCurrent() + + // verify that transition remains in preview stage and animates back to fromScene + assertTransition( + currentScene = SceneA, + isInPreviewStage = true, + previewProgress = 0.1f, + progress = 0f + ) + + // wait for the stop animation + advanceUntilIdle() + assertIdle(currentScene = SceneA) + } + + @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) @@ -940,7 +987,7 @@ class DraggableHandlerTest { } @Test - fun finish() = runGestureTest { + fun freezeAndAnimateToCurrentState() = runGestureTest { // Start at scene C. navigateToSceneC() @@ -952,35 +999,25 @@ class DraggableHandlerTest { // The current transition can be intercepted. assertThat(draggableHandler.shouldImmediatelyIntercept(middle)).isTrue() - // Finish the transition. + // Freeze the transition. val transition = transitionState as Transition - val job = transition.finish() + transition.freezeAndAnimateToCurrentState() assertTransition(isUserInputOngoing = false) - - // The current transition can not be intercepted anymore. - assertThat(draggableHandler.shouldImmediatelyIntercept(middle)).isFalse() - - // Calling finish() multiple times returns the same Job. - assertThat(transition.finish()).isSameInstanceAs(job) - assertThat(transition.finish()).isSameInstanceAs(job) - assertThat(transition.finish()).isSameInstanceAs(job) - - // We can join the job to wait for the animation to end. - assertTransition() - job.join() + advanceUntilIdle() assertIdle(SceneC) } @Test - fun finish_cancelled() = runGestureTest { - // Swipe up from the middle to transition to scene B. - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - onDragStarted(startedPosition = middle, overSlop = up(0.1f)) - assertTransition(fromScene = SceneA, toScene = SceneB) + fun interruptedTransitionCanNotBeImmediatelyIntercepted() = runGestureTest { + assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isFalse() + onDragStarted(overSlop = up(0.1f)) + assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue() - // Finish the transition and cancel the returned job. - (transitionState as Transition).finish().cancelAndJoin() - assertIdle(SceneA) + layoutState.startTransitionImmediately( + animationScope = testScope.backgroundScope, + transition(SceneA, SceneB) + ) + assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isFalse() } @Test diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 770c0f8dbb8f..60596de29f05 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -71,10 +71,11 @@ import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.assertSizeIsEqualTo +import com.android.compose.test.setContentAndCreateMainScope +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Ignore import org.junit.Rule @@ -504,7 +505,7 @@ class ElementTest { } @Test - fun elementModifierNodeIsRecycledInLazyLayouts() = runTest { + fun elementModifierNodeIsRecycledInLazyLayouts() { val nPages = 2 val pagerState = PagerState(currentPage = 0) { nPages } var nullableLayoutImpl: SceneTransitionLayoutImpl? = null @@ -630,18 +631,19 @@ class ElementTest { ) } - rule.setContent { - SceneTransitionLayout(state) { - scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) } - scene(SceneB) {} + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) } + scene(SceneB) {} + } } - } // Pause the clock to block recompositions. rule.mainClock.autoAdvance = false // Change the current transition. - rule.runOnUiThread { + scope.launch { state.startTransition(transition(from = SceneA, to = SceneB, progress = { 0.5f })) } @@ -1296,7 +1298,7 @@ class ElementTest { } @Test - fun interruption() = runTest { + fun interruption() { // 4 frames of animation. val duration = 4 * 16 @@ -1336,37 +1338,41 @@ class ElementTest { val valueInC = 200f lateinit var layoutImpl: SceneTransitionLayoutImpl - rule.setContent { - SceneTransitionLayoutForTesting( - state, - Modifier.size(layoutSize), - onLayoutImpl = { layoutImpl = it }, - ) { - // In scene A, Foo is aligned at the TopStart. - scene(SceneA) { - Box(Modifier.fillMaxSize()) { - Foo(sizeInA, valueInA, Modifier.align(Alignment.TopStart)) + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting( + state, + Modifier.size(layoutSize), + onLayoutImpl = { layoutImpl = it }, + ) { + // In scene A, Foo is aligned at the TopStart. + scene(SceneA) { + Box(Modifier.fillMaxSize()) { + Foo(sizeInA, valueInA, Modifier.align(Alignment.TopStart)) + } } - } - // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming - // from B. We put it before (below) scene B so that we can check that interruptions - // values and deltas are properly cleared once all transitions are done. - scene(SceneC) { - Box(Modifier.fillMaxSize()) { - Foo(sizeInC, valueInC, Modifier.align(Alignment.BottomEnd)) + // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when + // coming + // from B. We put it before (below) scene B so that we can check that + // interruptions + // values and deltas are properly cleared once all transitions are done. + scene(SceneC) { + Box(Modifier.fillMaxSize()) { + Foo(sizeInC, valueInC, Modifier.align(Alignment.BottomEnd)) + } } - } - // In scene B, Foo is aligned at the TopEnd, so it moves horizontally when coming - // from A. - scene(SceneB) { - Box(Modifier.fillMaxSize()) { - Foo(sizeInB, valueInB, Modifier.align(Alignment.TopEnd)) + // In scene B, Foo is aligned at the TopEnd, so it moves horizontally when + // coming + // from A. + scene(SceneB) { + Box(Modifier.fillMaxSize()) { + Foo(sizeInB, valueInB, Modifier.align(Alignment.TopEnd)) + } } } } - } // The offset of Foo when idle in A, B or C. val offsetInA = DpOffset.Zero @@ -1390,12 +1396,12 @@ class ElementTest { from = SceneA, to = SceneB, progress = { aToBProgress }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) val offsetInAToB = lerp(offsetInA, offsetInB, aToBProgress) val sizeInAToB = lerp(sizeInA, sizeInB, aToBProgress) val valueInAToB = lerp(valueInA, valueInB, aToBProgress) - rule.runOnUiThread { state.startTransition(aToB) } + scope.launch { state.startTransition(aToB) } rule .onNode(isElement(TestElements.Foo, SceneB)) .assertSizeIsEqualTo(sizeInAToB) @@ -1415,7 +1421,7 @@ class ElementTest { progress = { bToCProgress }, interruptionProgress = { interruptionProgress }, ) - rule.runOnUiThread { state.startTransition(bToC) } + scope.launch { state.startTransition(bToC) } // The interruption deltas, which will be multiplied by the interruption progress then added // to the current transition offset and size. @@ -1476,10 +1482,8 @@ class ElementTest { .assertSizeIsEqualTo(sizeInC) // Manually finish the transition. - rule.runOnUiThread { - state.finishTransition(aToB) - state.finishTransition(bToC) - } + aToB.finish() + bToC.finish() rule.waitForIdle() assertThat(state.transitionState).isIdle() @@ -1498,7 +1502,7 @@ class ElementTest { } @Test - fun interruption_sharedTransitionDisabled() = runTest { + fun interruption_sharedTransitionDisabled() { // 4 frames of animation. val duration = 4 * 16 val layoutSize = DpSize(200.dp, 100.dp) @@ -1524,21 +1528,22 @@ class ElementTest { Box(modifier.element(TestElements.Foo).size(fooSize)) } - rule.setContent { - SceneTransitionLayout(state, Modifier.size(layoutSize)) { - scene(SceneA) { - Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) } - } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state, Modifier.size(layoutSize)) { + scene(SceneA) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) } + } - scene(SceneB) { - Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopEnd)) } - } + scene(SceneB) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopEnd)) } + } - scene(SceneC) { - Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) } + scene(SceneC) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) } + } } } - } // The offset of Foo when idle in A, B or C. val offsetInA = DpOffset.Zero @@ -1547,7 +1552,12 @@ class ElementTest { // State is a transition A => B at 50% interrupted by B => C at 30%. val aToB = - transition(from = SceneA, to = SceneB, progress = { 0.5f }, onFinish = neverFinish()) + transition( + from = SceneA, + to = SceneB, + progress = { 0.5f }, + onFreezeAndAnimate = { /* never finish */ }, + ) var bToCInterruptionProgress by mutableStateOf(1f) val bToC = transition( @@ -1555,11 +1565,11 @@ class ElementTest { to = SceneC, progress = { 0.3f }, interruptionProgress = { bToCInterruptionProgress }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) - rule.runOnUiThread { state.startTransition(aToB) } + scope.launch { state.startTransition(aToB) } rule.waitForIdle() - rule.runOnUiThread { state.startTransition(bToC) } + scope.launch { state.startTransition(bToC) } // Foo is placed in both B and C given that the shared transition is disabled. In B, its // offset is impacted by the interruption but in C it is not. @@ -1579,7 +1589,8 @@ class ElementTest { // Manually finish A => B so only B => C is remaining. bToCInterruptionProgress = 0f - rule.runOnUiThread { state.finishTransition(aToB) } + aToB.finish() + rule .onNode(isElement(TestElements.Foo, SceneB)) .assertPositionInRootIsEqualTo(offsetInB.x, offsetInB.y) @@ -1595,7 +1606,7 @@ class ElementTest { progress = { 0.7f }, interruptionProgress = { 1f }, ) - rule.runOnUiThread { state.startTransition(bToA) } + scope.launch { state.startTransition(bToA) } // Foo should have the position it had in B right before the interruption. rule @@ -1609,32 +1620,35 @@ class ElementTest { val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl( - SceneA, - transitions { overscrollDisabled(SceneA, Orientation.Horizontal) } - ) - .apply { - startTransition( - transition( - from = SceneA, - to = SceneB, - progress = { -1f }, - orientation = Orientation.Horizontal - ) - ) - } + SceneA, + transitions { overscrollDisabled(SceneA, Orientation.Horizontal) } + ) } lateinit var layoutImpl: SceneTransitionLayoutImpl - rule.setContent { - SceneTransitionLayoutForTesting( - state, - Modifier.size(100.dp), - onLayoutImpl = { layoutImpl = it }, - ) { - scene(SceneA) {} - scene(SceneB) { Box(Modifier.element(TestElements.Foo)) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting( + state, + Modifier.size(100.dp), + onLayoutImpl = { layoutImpl = it }, + ) { + scene(SceneA) {} + scene(SceneB) { Box(Modifier.element(TestElements.Foo)) } + } } + + scope.launch { + state.startTransition( + transition( + from = SceneA, + to = SceneB, + progress = { -1f }, + orientation = Orientation.Horizontal + ) + ) } + rule.waitForIdle() assertThat(layoutImpl.elements).containsKey(TestElements.Foo) val foo = layoutImpl.elements.getValue(TestElements.Foo) @@ -1647,7 +1661,7 @@ class ElementTest { } @Test - fun lastAlphaIsNotSetByOutdatedLayer() = runTest { + fun lastAlphaIsNotSetByOutdatedLayer() { val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl( @@ -1657,23 +1671,24 @@ class ElementTest { } lateinit var layoutImpl: SceneTransitionLayoutImpl - rule.setContent { - SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { - scene(SceneA) {} - scene(SceneB) { Box(Modifier.element(TestElements.Foo)) } - scene(SceneC) { Box(Modifier.element(TestElements.Foo)) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { + scene(SceneA) {} + scene(SceneB) { Box(Modifier.element(TestElements.Foo)) } + scene(SceneC) { Box(Modifier.element(TestElements.Foo)) } + } } - } // Start A => B at 0.5f. var aToBProgress by mutableStateOf(0.5f) - rule.runOnUiThread { + scope.launch { state.startTransition( transition( from = SceneA, to = SceneB, progress = { aToBProgress }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) ) } @@ -1692,7 +1707,7 @@ class ElementTest { assertThat(fooInB.lastAlpha).isEqualTo(0.7f) // Start B => C at 0.3f. - rule.runOnUiThread { + scope.launch { state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0.3f })) } rule.waitForIdle() @@ -1720,16 +1735,17 @@ class ElementTest { } lateinit var layoutImpl: SceneTransitionLayoutImpl - rule.setContent { - SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { - scene(SceneA) {} - scene(SceneB) { Box(Modifier.element(TestElements.Foo)) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { + scene(SceneA) {} + scene(SceneB) { Box(Modifier.element(TestElements.Foo)) } + } } - } // Start A => B at 60%. var interruptionProgress by mutableStateOf(1f) - rule.runOnUiThread { + scope.launch { state.startTransition( transition( from = SceneA, @@ -1774,19 +1790,20 @@ class ElementTest { Box(Modifier.element(TestElements.Foo).size(10.dp)) } - rule.setContent { - SceneTransitionLayout(state) { - scene(SceneA) { Foo() } - scene(SceneB) { Foo() } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneA) { Foo() } + scene(SceneB) { Foo() } + } } - } rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsDisplayed() rule.onNode(isElement(TestElements.Foo, SceneB)).assertDoesNotExist() // A => B while overscrolling at scene B. var progress by mutableStateOf(2f) - rule.runOnUiThread { + scope.launch { state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress })) } rule.waitForIdle() @@ -1827,19 +1844,20 @@ class ElementTest { MovableElement(key, modifier) { content { Text(text) } } } - rule.setContent { - SceneTransitionLayout(state) { - scene(SceneA) { MovableFoo(text = fooInA) } - scene(SceneB) { MovableFoo(text = fooInB) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneA) { MovableFoo(text = fooInA) } + scene(SceneB) { MovableFoo(text = fooInB) } + } } - } rule.onNode(hasText(fooInA)).assertIsDisplayed() rule.onNode(hasText(fooInB)).assertDoesNotExist() // A => B while overscrolling at scene B. var progress by mutableStateOf(2f) - rule.runOnUiThread { + scope.launch { state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress })) } rule.waitForIdle() @@ -1858,7 +1876,7 @@ class ElementTest { } @Test - fun interruptionThenOverscroll() = runTest { + fun interruptionThenOverscroll() { val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl( @@ -1879,22 +1897,23 @@ class ElementTest { } } - rule.setContent { - SceneTransitionLayout(state, Modifier.size(200.dp)) { - scene(SceneA) { SceneWithFoo(offset = DpOffset.Zero) } - scene(SceneB) { SceneWithFoo(offset = DpOffset(x = 40.dp, y = 0.dp)) } - scene(SceneC) { SceneWithFoo(offset = DpOffset(x = 40.dp, y = 40.dp)) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { SceneWithFoo(offset = DpOffset.Zero) } + scene(SceneB) { SceneWithFoo(offset = DpOffset(x = 40.dp, y = 0.dp)) } + scene(SceneC) { SceneWithFoo(offset = DpOffset(x = 40.dp, y = 40.dp)) } + } } - } // Start A => B at 75%. - rule.runOnUiThread { + scope.launch { state.startTransition( transition( from = SceneA, to = SceneB, progress = { 0.75f }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) ) } @@ -1907,7 +1926,7 @@ class ElementTest { // Interrupt A => B with B => C at 0%. var progress by mutableStateOf(0f) var interruptionProgress by mutableStateOf(1f) - rule.runOnUiThread { + scope.launch { state.startTransition( transition( from = SceneB, @@ -1915,7 +1934,7 @@ class ElementTest { progress = { progress }, interruptionProgress = { interruptionProgress }, orientation = Orientation.Vertical, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) ) } @@ -1963,12 +1982,13 @@ class ElementTest { } lateinit var layoutImpl: SceneTransitionLayoutImpl - rule.setContent { - SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { - scene(SceneA) { NestedFooBar() } - scene(SceneB) { NestedFooBar() } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { + scene(SceneA) { NestedFooBar() } + scene(SceneB) { NestedFooBar() } + } } - } // Idle on A: composed and placed only in B. rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsDisplayed() @@ -1997,7 +2017,7 @@ class ElementTest { assertThat(barInA.lastScale).isNotEqualTo(Scale.Unspecified) // A => B: composed in both and placed only in B. - rule.runOnUiThread { state.startTransition(transition(from = SceneA, to = SceneB)) } + scope.launch { state.startTransition(transition(from = SceneA, to = SceneB)) } rule.onNode(isElement(TestElements.Foo, SceneA)).assertExists().assertIsNotDisplayed() rule.onNode(isElement(TestElements.Bar, SceneA)).assertExists().assertIsNotDisplayed() rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsDisplayed() @@ -2024,7 +2044,7 @@ class ElementTest { } @Test - fun currentTransitionSceneIsUsedToComputeElementValues() = runTest { + fun currentTransitionSceneIsUsedToComputeElementValues() { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl( @@ -2044,23 +2064,31 @@ class ElementTest { } } - rule.setContent { - SceneTransitionLayout(state, Modifier.size(200.dp)) { - scene(SceneA) { Foo() } - scene(SceneB) {} - scene(SceneC) { Foo() } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Foo() } + scene(SceneB) {} + scene(SceneC) { Foo() } + } } - } // We have 2 transitions: // - A => B at 100% // - B => C at 0% // So Foo should have a size of (40dp, 60dp) in both A and C given that it is scaling its // size in B => C. - rule.runOnUiThread { + scope.launch { state.startTransition( - transition(from = SceneA, to = SceneB, progress = { 1f }, onFinish = neverFinish()) + transition( + from = SceneA, + to = SceneB, + progress = { 1f }, + onFreezeAndAnimate = { /* never finish */ }, + ) ) + } + scope.launch { state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0f })) } @@ -2069,7 +2097,7 @@ class ElementTest { } @Test - fun interruptionDeltasAreProperlyCleaned() = runTest { + fun interruptionDeltasAreProperlyCleaned() { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } @Composable @@ -2079,18 +2107,24 @@ class ElementTest { } } - rule.setContent { - SceneTransitionLayout(state, Modifier.size(200.dp)) { - scene(SceneA) { Foo(offset = 0.dp) } - scene(SceneB) { Foo(offset = 20.dp) } - scene(SceneC) { Foo(offset = 40.dp) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Foo(offset = 0.dp) } + scene(SceneB) { Foo(offset = 20.dp) } + scene(SceneC) { Foo(offset = 40.dp) } + } } - } // Start A => B at 50%. val aToB = - transition(from = SceneA, to = SceneB, progress = { 0.5f }, onFinish = neverFinish()) - rule.runOnUiThread { state.startTransition(aToB) } + transition( + from = SceneA, + to = SceneB, + progress = { 0.5f }, + onFreezeAndAnimate = { /* never finish */ }, + ) + scope.launch { state.startTransition(aToB) } rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(10.dp, 10.dp) // Start B => C at 0%. This will compute an interruption delta of (-10dp, -10dp) so that the @@ -2103,9 +2137,9 @@ class ElementTest { current = { SceneB }, progress = { 0f }, interruptionProgress = { interruptionProgress }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) - rule.runOnUiThread { state.startTransition(bToC) } + scope.launch { state.startTransition(bToC) } rule.onNode(isElement(TestElements.Foo, SceneC)).assertPositionInRootIsEqualTo(10.dp, 10.dp) // Finish the interruption and leave the transition progress at 0f. We should be at the same @@ -2116,9 +2150,9 @@ class ElementTest { // Finish both transitions but directly start a new one B => A with interruption progress // 100%. We should be at (20dp, 20dp), unless the interruption deltas have not been // correctly cleaned. - rule.runOnUiThread { - state.finishTransition(aToB) - state.finishTransition(bToC) + aToB.finish() + bToC.finish() + scope.launch { state.startTransition( transition( from = SceneB, @@ -2132,7 +2166,7 @@ class ElementTest { } @Test - fun lastSizeIsUnspecifiedWhenOverscrollingOtherScene() = runTest { + fun lastSizeIsUnspecifiedWhenOverscrollingOtherScene() { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl( @@ -2147,17 +2181,23 @@ class ElementTest { } lateinit var layoutImpl: SceneTransitionLayoutImpl - rule.setContent { - SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { - scene(SceneA) { Foo() } - scene(SceneB) { Foo() } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { + scene(SceneA) { Foo() } + scene(SceneB) { Foo() } + } } - } // Overscroll A => B on A. - rule.runOnUiThread { + scope.launch { state.startTransition( - transition(from = SceneA, to = SceneB, progress = { -1f }, onFinish = neverFinish()) + transition( + from = SceneA, + to = SceneB, + progress = { -1f }, + onFreezeAndAnimate = { /* never finish */ }, + ) ) } rule.waitForIdle() @@ -2173,7 +2213,7 @@ class ElementTest { } @Test - fun transparentElementIsNotImpactingInterruption() = runTest { + fun transparentElementIsNotImpactingInterruption() { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl( @@ -2200,23 +2240,24 @@ class ElementTest { Box(modifier.element(TestElements.Foo).size(10.dp)) } - rule.setContent { - SceneTransitionLayout(state) { - scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) } - // Define A after B so that Foo is placed in A during A <=> B. - scene(SceneA) { Foo() } + // Define A after B so that Foo is placed in A during A <=> B. + scene(SceneA) { Foo() } + } } - } // Start A => B at 70%. - rule.runOnUiThread { + scope.launch { state.startTransition( transition( from = SceneA, to = SceneB, progress = { 0.7f }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) ) } @@ -2227,14 +2268,14 @@ class ElementTest { // Start B => A at 50% with interruptionProgress = 100%. Foo is placed in A and should still // be at (40dp, 60dp) given that it was fully transparent in A before the interruption. var interruptionProgress by mutableStateOf(1f) - rule.runOnUiThread { + scope.launch { state.startTransition( transition( from = SceneB, to = SceneA, progress = { 0.5f }, interruptionProgress = { interruptionProgress }, - onFinish = neverFinish(), + onFreezeAndAnimate = { /* never finish */ }, ) ) } @@ -2250,7 +2291,7 @@ class ElementTest { } @Test - fun replacedTransitionDoesNotTriggerInterruption() = runTest { + fun replacedTransitionDoesNotTriggerInterruption() { val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } @Composable @@ -2258,17 +2299,23 @@ class ElementTest { Box(modifier.element(TestElements.Foo).size(10.dp)) } - rule.setContent { - SceneTransitionLayout(state) { - scene(SceneA) { Foo() } - scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state) { + scene(SceneA) { Foo() } + scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) } + } } - } // Start A => B at 50%. val aToB1 = - transition(from = SceneA, to = SceneB, progress = { 0.5f }, onFinish = neverFinish()) - rule.runOnUiThread { state.startTransition(aToB1) } + transition( + from = SceneA, + to = SceneB, + progress = { 0.5f }, + onFreezeAndAnimate = { /* never finish */ }, + ) + scope.launch { state.startTransition(aToB1) } rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsNotDisplayed() rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(20.dp, 30.dp) @@ -2282,7 +2329,7 @@ class ElementTest { interruptionProgress = { 1f }, replacedTransition = aToB1, ) - rule.runOnUiThread { state.startTransition(aToB2) } + scope.launch { state.startTransition(aToB2) } rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsNotDisplayed() rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(40.dp, 60.dp) } @@ -2428,12 +2475,13 @@ class ElementTest { } lateinit var layoutImpl: SceneTransitionLayoutImpl - rule.setContent { - SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { - scene(from) { Box { exitingElements.forEach { Foo(it) } } } - scene(to) { Box { enteringElements.forEach { Foo(it) } } } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) { + scene(from) { Box { exitingElements.forEach { Foo(it) } } } + scene(to) { Box { enteringElements.forEach { Foo(it) } } } + } } - } val bToA = transition( @@ -2443,7 +2491,7 @@ class ElementTest { previewProgress = { previewProgress }, isInPreviewStage = { isInPreviewStage } ) - rule.runOnUiThread { state.startTransition(bToA) } + scope.launch { state.startTransition(bToA) } rule.waitForIdle() return layoutImpl } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt index 3f6bd2c38792..bc929bd6b4ce 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt @@ -25,9 +25,9 @@ import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.runMonotonicClockTest +import com.android.compose.test.transition import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -44,8 +44,8 @@ class InterruptionHandlerTest { transitions { /* default interruption handler */ }, ) - state.setTargetScene(SceneB, coroutineScope = this) - state.setTargetScene(SceneC, coroutineScope = this) + state.setTargetScene(SceneB, animationScope = this) + state.setTargetScene(SceneC, animationScope = this) assertThat(state.currentTransitions) .comparingElementsUsing(FromToCurrentTriple) @@ -81,8 +81,8 @@ class InterruptionHandlerTest { }, ) - state.setTargetScene(SceneB, coroutineScope = this) - state.setTargetScene(SceneC, coroutineScope = this) + state.setTargetScene(SceneB, animationScope = this) + state.setTargetScene(SceneC, animationScope = this) assertThat(state.currentTransitions) .comparingElementsUsing(FromToCurrentTriple) @@ -124,10 +124,10 @@ class InterruptionHandlerTest { // Animate to B and advance the transition a little bit so that progress > visibility // threshold and that reversing from B back to A won't immediately snap to A. - state.setTargetScene(SceneB, coroutineScope = this) + state.setTargetScene(SceneB, animationScope = this) testScheduler.advanceTimeBy(duration / 2L) - state.setTargetScene(SceneC, coroutineScope = this) + state.setTargetScene(SceneC, animationScope = this) assertThat(state.currentTransitions) .comparingElementsUsing(FromToCurrentTriple) @@ -155,13 +155,21 @@ class InterruptionHandlerTest { // Progress must be > visibility threshold otherwise we will directly snap to A. progress = { 0.5f }, progressVelocity = { progressVelocity }, - onFinish = { launch {} }, ) - state.startTransition(aToB) + state.startTransitionImmediately(animationScope = backgroundScope, aToB) // Animate back to A. The previous transition is reversed, i.e. it has the same (from, to) // pair, and its velocity is used when animating the progress back to 0. - val bToA = checkNotNull(state.setTargetScene(SceneA, coroutineScope = this)) + val bToA = + checkNotNull( + state.setTargetScene( + SceneA, + // We use testScope here and not backgroundScope because setTargetScene + // needs the monotonic clock that is only available in the test scope. + animationScope = this, + ) + ) + .first testScheduler.runCurrent() assertThat(bToA).hasFromScene(SceneA) assertThat(bToA).hasToScene(SceneB) @@ -181,13 +189,21 @@ class InterruptionHandlerTest { to = SceneB, current = { SceneA }, progressVelocity = { progressVelocity }, - onFinish = { launch {} }, ) - state.startTransition(aToB) + state.startTransitionImmediately(animationScope = backgroundScope, aToB) // Animate to B. The previous transition is reversed, i.e. it has the same (from, to) pair, // and its velocity is used when animating the progress to 1. - val bToA = checkNotNull(state.setTargetScene(SceneB, coroutineScope = this)) + val bToA = + checkNotNull( + state.setTargetScene( + SceneB, + // We use testScope here and not backgroundScope because setTargetScene + // needs the monotonic clock that is only available in the test scope. + animationScope = this, + ) + ) + .first testScheduler.runCurrent() assertThat(bToA).hasFromScene(SceneA) assertThat(bToA).hasToScene(SceneB) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementContentPickerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementContentPickerTest.kt index e1d09453ee64..c8e7e6592e17 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementContentPickerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementContentPickerTest.kt @@ -17,6 +17,7 @@ package com.android.compose.animation.scene import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows import org.junit.Test diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt index 0543e7f09e5d..f3161f36bbf0 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch @@ -89,16 +90,16 @@ class ObservableTransitionStateTest { at(0) { val state = observableState() assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java) - assertThat((state as ObservableTransitionState.Transition).fromScene) + assertThat((state as ObservableTransitionState.Transition).fromContent) .isEqualTo(SceneA) - assertThat(state.toScene).isEqualTo(SceneB) + assertThat(state.toContent).isEqualTo(SceneB) assertThat(state.progress()).isEqualTo(0f) } at(TestTransitionDuration / 2) { val state = observableState() - assertThat((state as ObservableTransitionState.Transition).fromScene) + assertThat((state as ObservableTransitionState.Transition).fromContent) .isEqualTo(SceneA) - assertThat(state.toScene).isEqualTo(SceneB) + assertThat(state.toContent).isEqualTo(SceneB) assertThat(state.progress()).isEqualTo(0.5f) } after { @@ -139,7 +140,7 @@ class ObservableTransitionStateTest { var transitionCurrentScene by mutableStateOf(SceneA) val transition = transition(from = SceneA, to = SceneB, current = { transitionCurrentScene }) - state.startTransition(transition) + state.startTransitionImmediately(animationScope = backgroundScope, transition) assertThat(currentScene.value).isEqualTo(SceneA) // Change the transition current scene. @@ -199,7 +200,7 @@ class ObservableTransitionStateTest { var state = observableState() assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java) - assertThat((state as ObservableTransitionState.Transition).fromScene).isEqualTo(SceneA) + assertThat((state as ObservableTransitionState.Transition).fromContent).isEqualTo(SceneA) assertThat(state.previewProgress()).isEqualTo(0.4f) assertThat(state.isInPreviewStage()).isEqualTo(true) @@ -217,7 +218,7 @@ class ObservableTransitionStateTest { } state = observableState() assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java) - assertThat((state as ObservableTransitionState.Transition).fromScene).isEqualTo(SceneA) + assertThat((state as ObservableTransitionState.Transition).fromContent).isEqualTo(SceneA) assertThat(state.previewProgress()).isEqualTo(0.4f) assertThat(state.isInPreviewStage()).isEqualTo(false) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt index c5b6cdf12385..9284ffddcee3 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt @@ -18,6 +18,8 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size @@ -65,7 +67,23 @@ class PredictiveBackHandlerTest { @Test fun testPredictiveBack() { - val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + val transitionFrames = 2 + val layoutState = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + transitions = + transitions { + from(SceneA, to = SceneB) { + spec = + tween( + durationMillis = transitionFrames * 16, + easing = LinearEasing + ) + } + } + ) + } rule.setContent { SceneTransitionLayout(layoutState) { scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) } @@ -94,12 +112,27 @@ class PredictiveBackHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).isIdle() + rule.mainClock.autoAdvance = false + // Start again and commit it. rule.runOnUiThread { dispatcher.dispatchOnBackStarted(backEvent()) dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f)) dispatcher.onBackPressed() } + rule.mainClock.advanceTimeByFrame() + rule.waitForIdle() + val transition2 = assertThat(layoutState.transitionState).isSceneTransition() + // verify that transition picks up progress from preview + assertThat(transition2).hasProgress(0.4f, tolerance = 0.0001f) + + rule.mainClock.advanceTimeByFrame() + rule.waitForIdle() + // verify that transition is half way between preview-end-state (0.4f) and target-state (1f) + // after one frame + assertThat(transition2).hasProgress(0.7f, tolerance = 0.0001f) + + rule.mainClock.autoAdvance = true rule.waitForIdle() assertThat(layoutState.transitionState).hasCurrentScene(SceneB) assertThat(layoutState.transitionState).isIdle() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 69f2cbace276..cd20a29a05d9 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -30,14 +30,18 @@ import com.android.compose.animation.scene.TestScenes.SceneD import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.animation.scene.transition.link.StateLink +import com.android.compose.animation.scene.transition.seekToScene +import com.android.compose.test.MonotonicClockTestScope +import com.android.compose.test.TestTransition import com.android.compose.test.runMonotonicClockTest +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat +import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -58,9 +62,12 @@ class SceneTransitionLayoutStateTest { } @Test - fun isTransitioningTo_transition() { + fun isTransitioningTo_transition() = runTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) - state.startTransition(transition(from = SceneA, to = SceneB)) + state.startTransitionImmediately( + animationScope = backgroundScope, + transition(from = SceneA, to = SceneB) + ) assertThat(state.isTransitioning()).isTrue() assertThat(state.isTransitioning(from = SceneA)).isTrue() @@ -73,17 +80,16 @@ class SceneTransitionLayoutStateTest { @Test fun setTargetScene_idleToSameScene() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutState(SceneA) - assertThat(state.setTargetScene(SceneA, coroutineScope = this)).isNull() + assertThat(state.setTargetScene(SceneA, animationScope = this)).isNull() } @Test fun setTargetScene_idleToDifferentScene() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutState(SceneA) - val transition = state.setTargetScene(SceneB, coroutineScope = this) - assertThat(transition).isNotNull() + val (transition, job) = checkNotNull(state.setTargetScene(SceneB, animationScope = this)) assertThat(state.transitionState).isEqualTo(transition) - transition!!.finish().join() + job.join() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) } @@ -91,11 +97,10 @@ class SceneTransitionLayoutStateTest { fun setTargetScene_transitionToSameScene() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutState(SceneA) - val transition = state.setTargetScene(SceneB, coroutineScope = this) - assertThat(transition).isNotNull() - assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNull() + val (_, job) = checkNotNull(state.setTargetScene(SceneB, animationScope = this)) + assertThat(state.setTargetScene(SceneB, animationScope = this)).isNull() - transition!!.finish().join() + job.join() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) } @@ -103,11 +108,10 @@ class SceneTransitionLayoutStateTest { fun setTargetScene_transitionToDifferentScene() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutState(SceneA) - assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() - val transition = state.setTargetScene(SceneC, coroutineScope = this) - assertThat(transition).isNotNull() + assertThat(state.setTargetScene(SceneB, animationScope = this)).isNotNull() + val (_, job) = checkNotNull(state.setTargetScene(SceneC, animationScope = this)) - transition!!.finish().join() + job.join() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneC)) } @@ -118,7 +122,7 @@ class SceneTransitionLayoutStateTest { lateinit var transition: TransitionState.Transition val job = launch(start = CoroutineStart.UNDISPATCHED) { - transition = state.setTargetScene(SceneB, coroutineScope = this)!! + transition = checkNotNull(state.setTargetScene(SceneB, animationScope = this)).first } assertThat(state.transitionState).isEqualTo(transition) @@ -127,18 +131,6 @@ class SceneTransitionLayoutStateTest { assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) } - @Test - fun transition_finishReturnsTheSameJobWhenCalledMultipleTimes() = runMonotonicClockTest { - val state = MutableSceneTransitionLayoutState(SceneA) - val transition = state.setTargetScene(SceneB, coroutineScope = this) - assertThat(transition).isNotNull() - - val job = transition!!.finish() - assertThat(transition.finish()).isSameInstanceAs(job) - assertThat(transition.finish()).isSameInstanceAs(job) - assertThat(transition.finish()).isSameInstanceAs(job) - } - private fun setupLinkedStates( parentInitialScene: SceneKey = SceneC, childInitialScene: SceneKey = SceneA, @@ -163,22 +155,24 @@ class SceneTransitionLayoutStateTest { } @Test - fun linkedTransition_startsLinkAndFinishesLinkInToState() { + fun linkedTransition_startsLinkAndFinishesLinkInToState() = runTest { val (parentState, childState) = setupLinkedStates() val childTransition = transition(SceneA, SceneB) - childState.startTransition(childTransition) + val job = + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue() assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue() - childState.finishTransition(childTransition) + childTransition.finish() + job.join() assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD)) } @Test - fun linkedTransition_transitiveLink() { + fun linkedTransition_transitiveLink() = runTest { val parentParentState = MutableSceneTransitionLayoutState(SceneB) as MutableSceneTransitionLayoutStateImpl val parentLink = @@ -204,25 +198,27 @@ class SceneTransitionLayoutStateTest { val childTransition = transition(SceneA, SceneB) - childState.startTransition(childTransition) + val job = + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue() assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue() assertThat(parentParentState.isTransitioning(SceneB, SceneC)).isTrue() - childState.finishTransition(childTransition) + childTransition.finish() + job.join() assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD)) assertThat(parentParentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) } @Test - fun linkedTransition_linkProgressIsEqual() { + fun linkedTransition_linkProgressIsEqual() = runTest { val (parentState, childState) = setupLinkedStates() var progress = 0f val childTransition = transition(SceneA, SceneB, progress = { progress }) - childState.startTransition(childTransition) + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) assertThat(parentState.currentTransition?.progress).isEqualTo(0f) progress = .5f @@ -230,28 +226,32 @@ class SceneTransitionLayoutStateTest { } @Test - fun linkedTransition_reverseTransitionIsNotLinked() { + fun linkedTransition_reverseTransitionIsNotLinked() = runTest { val (parentState, childState) = setupLinkedStates() val childTransition = transition(SceneB, SceneA, current = { SceneB }) - childState.startTransition(childTransition) + val job = + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) assertThat(childState.isTransitioning(SceneB, SceneA)).isTrue() assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) - childState.finishTransition(childTransition) + childTransition.finish() + job.join() assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) } @Test - fun linkedTransition_startsLinkAndFinishesLinkInFromState() { + fun linkedTransition_startsLinkAndFinishesLinkInFromState() = runTest { val (parentState, childState) = setupLinkedStates() val childTransition = transition(SceneA, SceneB, current = { SceneA }) - childState.startTransition(childTransition) + val job = + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) - childState.finishTransition(childTransition) + childTransition.finish() + job.join() assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA)) assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) } @@ -260,22 +260,14 @@ class SceneTransitionLayoutStateTest { fun linkedTransition_startsLinkButLinkedStateIsTakenOver() = runTest { val (parentState, childState) = setupLinkedStates() - val childTransition = - transition( - SceneA, - SceneB, - onFinish = { launch { /* Do nothing. */ } }, - ) - val parentTransition = - transition( - SceneC, - SceneA, - onFinish = { launch { /* Do nothing. */ } }, - ) - childState.startTransition(childTransition) - parentState.startTransition(parentTransition) + val childTransition = transition(SceneA, SceneB) + val parentTransition = transition(SceneC, SceneA) + val job = + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) + parentState.startTransitionImmediately(animationScope = backgroundScope, parentTransition) - childState.finishTransition(childTransition) + childTransition.finish() + job.join() assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) assertThat(parentState.transitionState).isEqualTo(parentTransition) } @@ -297,11 +289,11 @@ class SceneTransitionLayoutStateTest { ) // Default transition from A to B. - assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() + assertThat(state.setTargetScene(SceneB, animationScope = this)).isNotNull() assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1) // Go back to A. - state.setTargetScene(SceneA, coroutineScope = this) + state.setTargetScene(SceneA, animationScope = this) testScheduler.advanceUntilIdle() assertThat(state.transitionState).isIdle() assertThat(state.transitionState).hasCurrentScene(SceneA) @@ -310,7 +302,7 @@ class SceneTransitionLayoutStateTest { assertThat( state.setTargetScene( SceneB, - coroutineScope = this, + animationScope = this, transitionKey = transitionkey, ) ) @@ -321,7 +313,8 @@ class SceneTransitionLayoutStateTest { @Test fun snapToIdleIfClose_snapToStart() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) - state.startTransition( + state.startTransitionImmediately( + animationScope = backgroundScope, transition(from = SceneA, to = SceneB, current = { SceneA }, progress = { 0.2f }) ) assertThat(state.isTransitioning()).isTrue() @@ -339,7 +332,10 @@ class SceneTransitionLayoutStateTest { @Test fun snapToIdleIfClose_snapToEnd() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) - state.startTransition(transition(from = SceneA, to = SceneB, progress = { 0.8f })) + state.startTransitionImmediately( + animationScope = backgroundScope, + transition(from = SceneA, to = SceneB, progress = { 0.8f }) + ) assertThat(state.isTransitioning()).isTrue() // Ignore the request if the progress is not close to 0 or 1, using the threshold. @@ -356,18 +352,12 @@ class SceneTransitionLayoutStateTest { fun snapToIdleIfClose_multipleTransitions() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) - val aToB = - transition( - from = SceneA, - to = SceneB, - progress = { 0.5f }, - onFinish = { launch { /* do nothing */ } }, - ) - state.startTransition(aToB) + val aToB = transition(from = SceneA, to = SceneB, progress = { 0.5f }) + state.startTransitionImmediately(animationScope = backgroundScope, aToB) assertThat(state.currentTransitions).containsExactly(aToB).inOrder() val bToC = transition(from = SceneB, to = SceneC, progress = { 0.8f }) - state.startTransition(bToC) + state.startTransitionImmediately(animationScope = backgroundScope, bToC) assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder() // Ignore the request if the progress is not close to 0 or 1, using the threshold. @@ -385,7 +375,8 @@ class SceneTransitionLayoutStateTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) var progress by mutableStateOf(0f) var currentScene by mutableStateOf(SceneB) - state.startTransition( + state.startTransitionImmediately( + animationScope = backgroundScope, transition( from = SceneA, to = SceneB, @@ -406,47 +397,51 @@ class SceneTransitionLayoutStateTest { } @Test - fun linkedTransition_fuzzyLinksAreMatchedAndStarted() { + fun linkedTransition_fuzzyLinksAreMatchedAndStarted() = runTest { val (parentState, childState) = setupLinkedStates(SceneC, SceneA, null, null, null, SceneD) val childTransition = transition(SceneA, SceneB) - childState.startTransition(childTransition) + val job = + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue() assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue() - childState.finishTransition(childTransition) + childTransition.finish() + job.join() assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneB)) assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneD)) } @Test - fun linkedTransition_fuzzyLinksAreMatchedAndResetToProperPreviousScene() { + fun linkedTransition_fuzzyLinksAreMatchedAndResetToProperPreviousScene() = runTest { val (parentState, childState) = setupLinkedStates(SceneC, SceneA, SceneA, null, null, SceneD) val childTransition = transition(SceneA, SceneB, current = { SceneA }) - childState.startTransition(childTransition) + val job = + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue() assertThat(parentState.isTransitioning(SceneC, SceneD)).isTrue() - childState.finishTransition(childTransition) + childTransition.finish() + job.join() assertThat(childState.transitionState).isEqualTo(TransitionState.Idle(SceneA)) assertThat(parentState.transitionState).isEqualTo(TransitionState.Idle(SceneC)) } @Test - fun linkedTransition_fuzzyLinksAreNotMatched() { + fun linkedTransition_fuzzyLinksAreNotMatched() = runTest { val (parentState, childState) = setupLinkedStates(SceneC, SceneA, SceneB, null, SceneC, SceneD) val childTransition = transition(SceneA, SceneB) - childState.startTransition(childTransition) + childState.startTransitionImmediately(animationScope = backgroundScope, childTransition) assertThat(childState.isTransitioning(SceneA, SceneB)).isTrue() assertThat(parentState.isTransitioning(SceneC, SceneD)).isFalse() } - private fun startOverscrollableTransistionFromAtoB( + private fun MonotonicClockTestScope.startOverscrollableTransistionFromAtoB( progress: () -> Float, sceneTransitions: SceneTransitions, ): MutableSceneTransitionLayoutStateImpl { @@ -455,7 +450,8 @@ class SceneTransitionLayoutStateTest { SceneA, sceneTransitions, ) - state.startTransition( + state.startTransitionImmediately( + animationScope = backgroundScope, transition( from = SceneA, to = SceneB, @@ -560,54 +556,54 @@ class SceneTransitionLayoutStateTest { @Test fun multipleTransitions() = runTest { - val finishingTransitions = mutableSetOf<TransitionState.Transition>() - fun onFinish(transition: TransitionState.Transition): Job { - // Instead of letting the transition finish, we put the transition in the - // finishingTransitions set so that we can verify that finish() is called when expected - // and then we call state STLState.finishTransition() ourselves. - finishingTransitions.add(transition) - - return backgroundScope.launch { - // Try to acquire a locked mutex so that this code never completes. - Mutex(locked = true).withLock {} - } + val frozenTransitions = mutableSetOf<TestTransition>() + fun onFreezeAndAnimate(transition: TestTransition): () -> Unit { + // Instead of letting the transition finish when it is frozen, we put the transition in + // the frozenTransitions set so that we can verify that freezeAndAnimateToCurrentState() + // is called when expected and then we call finish() ourselves to finish the + // transitions. + frozenTransitions.add(transition) + + return { /* do nothing */ } } val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions) - val aToB = transition(SceneA, SceneB, onFinish = ::onFinish) - val bToC = transition(SceneB, SceneC, onFinish = ::onFinish) - val cToA = transition(SceneC, SceneA, onFinish = ::onFinish) + val aToB = transition(SceneA, SceneB, onFreezeAndAnimate = ::onFreezeAndAnimate) + val bToC = transition(SceneB, SceneC, onFreezeAndAnimate = ::onFreezeAndAnimate) + val cToA = transition(SceneC, SceneA, onFreezeAndAnimate = ::onFreezeAndAnimate) // Starting state. - assertThat(finishingTransitions).isEmpty() + assertThat(frozenTransitions).isEmpty() assertThat(state.currentTransitions).isEmpty() // A => B. - state.startTransition(aToB) - assertThat(finishingTransitions).isEmpty() + val aToBJob = state.startTransitionImmediately(animationScope = backgroundScope, aToB) + assertThat(frozenTransitions).isEmpty() assertThat(state.finishedTransitions).isEmpty() assertThat(state.currentTransitions).containsExactly(aToB).inOrder() - // B => C. This should automatically call finish() on aToB. - state.startTransition(bToC) - assertThat(finishingTransitions).containsExactly(aToB) + // B => C. This should automatically call freezeAndAnimateToCurrentState() on aToB. + val bToCJob = state.startTransitionImmediately(animationScope = backgroundScope, bToC) + assertThat(frozenTransitions).containsExactly(aToB) assertThat(state.finishedTransitions).isEmpty() assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder() - // C => A. This should automatically call finish() on bToC. - state.startTransition(cToA) - assertThat(finishingTransitions).containsExactly(aToB, bToC) + // C => A. This should automatically call freezeAndAnimateToCurrentState() on bToC. + state.startTransitionImmediately(animationScope = backgroundScope, cToA) + assertThat(frozenTransitions).containsExactly(aToB, bToC) assertThat(state.finishedTransitions).isEmpty() assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder() // Mark bToC as finished. The list of current transitions does not change because aToB is // still not marked as finished. - state.finishTransition(bToC) + bToC.finish() + bToCJob.join() assertThat(state.finishedTransitions).containsExactly(bToC) assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder() // Mark aToB as finished. This will remove both aToB and bToC from the list of transitions. - state.finishTransition(aToB) + aToB.finish() + aToBJob.join() assertThat(state.finishedTransitions).isEmpty() assertThat(state.currentTransitions).containsExactly(cToA).inOrder() } @@ -617,8 +613,9 @@ class SceneTransitionLayoutStateTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions) fun startTransition() { - val transition = transition(SceneA, SceneB, onFinish = { launch { /* do nothing */ } }) - state.startTransition(transition) + val transition = + transition(SceneA, SceneB, onFreezeAndAnimate = { launch { /* do nothing */ } }) + state.startTransitionImmediately(animationScope = backgroundScope, transition) } var hasLoggedWtf = false @@ -641,7 +638,7 @@ class SceneTransitionLayoutStateTest { val state = MutableSceneTransitionLayoutState(SceneA) // Transition to B. - state.setTargetScene(SceneB, coroutineScope = this) + state.setTargetScene(SceneB, animationScope = this) val transition = assertThat(state.transitionState).isSceneTransition() assertThat(transition).hasCurrentScene(SceneB) @@ -650,4 +647,92 @@ class SceneTransitionLayoutStateTest { assertThat(state.transitionState).isIdle() assertThat(state.transitionState).hasCurrentScene(SceneC) } + + @Test + fun snapToScene_freezesCurrentTransition() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutStateImpl(SceneA) + + // Start a transition that is never finished. We don't use backgroundScope on purpose so + // that this test would fail if the transition was not frozen when snapping. + state.startTransitionImmediately(animationScope = this, transition(SceneA, SceneB)) + val transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + + // Snap to C. + state.snapToScene(SceneC) + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneC) + } + + @Test + fun seekToScene() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutState(SceneA) + val progress = Channel<Float>() + + val job = + launch(start = CoroutineStart.UNDISPATCHED) { + state.seekToScene(SceneB, progress.consumeAsFlow()) + } + + val transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasProgress(0f) + + // Change progress. + progress.send(0.4f) + assertThat(transition).hasProgress(0.4f) + + // Close the channel normally to confirm the transition. + progress.close() + job.join() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneB) + } + + @Test + fun seekToScene_cancelled() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutState(SceneA) + val progress = Channel<Float>() + + val job = + launch(start = CoroutineStart.UNDISPATCHED) { + state.seekToScene(SceneB, progress.consumeAsFlow()) + } + + val transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasProgress(0f) + + // Change progress. + progress.send(0.4f) + assertThat(transition).hasProgress(0.4f) + + // Close the channel with a CancellationException to cancel the transition. + progress.close(CancellationException()) + job.join() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + } + + @Test + fun seekToScene_interrupted() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutState(SceneA) + val progress = Channel<Float>() + + val job = + launch(start = CoroutineStart.UNDISPATCHED) { + state.seekToScene(SceneB, progress.consumeAsFlow()) + } + + assertThat(state.transitionState).isSceneTransition() + + // Start a new transition, interrupting the seek transition. + state.setTargetScene(SceneB, animationScope = this) + + // The previous job is cancelled and does not infinitely collect the progress. + job.join() + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index b8e13dab913b..63ab04ffe885 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -55,10 +55,13 @@ import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.assertSizeIsEqualTo +import com.android.compose.test.setContentAndCreateMainScope import com.android.compose.test.subjects.DpOffsetSubject import com.android.compose.test.subjects.assertThat +import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test @@ -327,17 +330,18 @@ class SceneTransitionLayoutTest { } val layoutTag = "layout" - rule.setContent { - SceneTransitionLayout(state, Modifier.testTag(layoutTag)) { - scene(SceneA) { Box(Modifier.size(50.dp)) } - scene(SceneB) { Box(Modifier.size(70.dp)) } + val scope = + rule.setContentAndCreateMainScope { + SceneTransitionLayout(state, Modifier.testTag(layoutTag)) { + scene(SceneA) { Box(Modifier.size(50.dp)) } + scene(SceneB) { Box(Modifier.size(70.dp)) } + } } - } // Overscroll on A at -100%: size should be interpolated given that there is no overscroll // defined for scene A. var progress by mutableStateOf(-1f) - rule.runOnIdle { + scope.launch { state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress })) } rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(30.dp) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt index 46075c3b3f9f..5bfc947758ca 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.testTransition -import com.android.compose.animation.scene.transition import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/SetContentAndCreateScope.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/SetContentAndCreateScope.kt new file mode 100644 index 000000000000..28a864f8f905 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/SetContentAndCreateScope.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 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.compose.test + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +/** + * Set [content] as this rule's content and return a [CoroutineScope] bound to [Dispatchers.Main] + * and scoped to this rule. + */ +fun ComposeContentTestRule.setContentAndCreateMainScope( + content: @Composable () -> Unit, +): CoroutineScope { + lateinit var coroutineScope: CoroutineScope + setContent { + coroutineScope = rememberCoroutineScope(getContext = { Dispatchers.Main }) + content() + } + return coroutineScope +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestTransition.kt index 467031afb262..a6a83eedb2ac 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/test/TestTransition.kt @@ -14,17 +14,35 @@ * limitations under the License. */ -package com.android.compose.animation.scene +package com.android.compose.test import androidx.compose.foundation.gestures.Orientation +import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.test.TestScope +import com.android.compose.animation.scene.content.state.TransitionState.Transition +import kotlinx.coroutines.CompletableDeferred -/** A utility to easily create a [TransitionState.Transition] in tests. */ +/** A transition for tests that will be finished once [finish] is called. */ +abstract class TestTransition( + fromScene: SceneKey, + toScene: SceneKey, + replacedTransition: Transition?, +) : Transition.ChangeScene(fromScene, toScene, replacedTransition) { + private val finishCompletable = CompletableDeferred<Unit>() + + override suspend fun run() { + finishCompletable.await() + } + + /** Finish this transition. */ + fun finish() { + finishCompletable.complete(Unit) + } +} + +/** A utility to easily create a [TestTransition] in tests. */ fun transition( from: SceneKey, to: SceneKey, @@ -40,12 +58,11 @@ fun transition( isUpOrLeft: Boolean = false, bouncingContent: ContentKey? = null, orientation: Orientation = Orientation.Horizontal, - onFinish: ((TransitionState.Transition) -> Job)? = null, - replacedTransition: TransitionState.Transition? = null, -): TransitionState.Transition.ChangeScene { + onFreezeAndAnimate: ((TestTransition) -> Unit)? = null, + replacedTransition: Transition? = null, +): TestTransition { return object : - TransitionState.Transition.ChangeScene(from, to, replacedTransition), - TransitionState.HasOverscrollProperties { + TestTransition(from, to, replacedTransition), TransitionState.HasOverscrollProperties { override val currentScene: SceneKey get() = current() @@ -71,14 +88,12 @@ fun transition( override val orientation: Orientation = orientation override val absoluteDistance = 0f - override fun finish(): Job { - val onFinish = - onFinish - ?: error( - "onFinish() must be provided if finish() is called on test transitions" - ) - - return onFinish(this) + override fun freezeAndAnimateToCurrentState() { + if (onFreezeAndAnimate != null) { + onFreezeAndAnimate(this) + } else { + finish() + } } override fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float { @@ -86,16 +101,3 @@ fun transition( } } } - -/** - * Return a onFinish lambda that can be used with [transition] so that the transition never - * finishes. This allows to keep the transition in the current transitions list. - */ -fun TestScope.neverFinish(): (TransitionState.Transition) -> Job { - return { - backgroundScope.launch { - // Try to acquire a locked mutex so that this code never completes. - Mutex(locked = true).withLock {} - } - } -} diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt index c5a5173cb037..25f95645e699 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt @@ -246,7 +246,7 @@ fun MotionTestRule<ComposeToolkit>.recordTransition( content = { play -> LaunchedEffect(play) { if (play) { - state.setTargetScene(toScene, coroutineScope = this) + state.setTargetScene(toScene, animationScope = this) } } @@ -284,7 +284,7 @@ fun ComposeContentTestRule.testTransition( testTransition( state = state, - changeState = { state -> state.setTargetScene(to, coroutineScope = this) }, + changeState = { state -> state.setTargetScene(to, animationScope = this) }, transitionLayout = transitionLayout, builder = builder, ) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt index 362e23dd9641..96d79df2124c 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepository.kt @@ -71,7 +71,7 @@ class NotificationSettingsRepository( .stateIn( scope = backgroundScope, started = SharingStarted.Eagerly, - initialValue = false, + initialValue = true, ) /** The default duration for DND mode when enabled. See [Settings.Secure.ZEN_DURATION]. */ diff --git a/packages/SystemUI/docs/scene.md b/packages/SystemUI/docs/scene.md index 2f50bbd66d16..a7740c677d51 100644 --- a/packages/SystemUI/docs/scene.md +++ b/packages/SystemUI/docs/scene.md @@ -121,7 +121,7 @@ Should a variant owner or OEM want to replace or add a new scene, they could do so by defining their own scene. This section describes how to do that. Each scene is defined as an implementation of the -[`ComposableScene`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposableScene.kt) +[`Scene`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/Scene.kt) interface, which has three parts: 1. The `key` property returns the [`SceneKey`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt) that uniquely identifies that scene 2. The `destinationScenes` `Flow` returns @@ -138,7 +138,7 @@ see the [Scene transition animations](#Scene-transition-animations) section For example: ```kotlin -@SysUISingleton class YourScene @Inject constructor( /* your dependencies here */ ) : ComposableScene { +@SysUISingleton class YourScene @Inject constructor( /* your dependencies here */ ) : Scene { override val key = SceneKey.YourScene override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> = diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index fe9105ed195e..062d351100d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -84,6 +84,7 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { private lateinit var mKeyguardMessageAreaController: KeyguardMessageAreaController<BouncerKeyguardMessageArea> @Mock private lateinit var postureController: DevicePostureController + @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier @Captor private lateinit var keyListenerArgumentCaptor: ArgumentCaptor<View.OnKeyListener> private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController @@ -131,6 +132,8 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { fakeFeatureFlags, mSelectedUserInteractor, keyguardKeyboardInteractor, + null, + mUserActivityNotifier ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index 0054d137bd2c..1076d9089d79 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -87,6 +87,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { private View mOkButton; @Mock private SelectedUserInteractor mSelectedUserInteractor; + @Mock + private UserActivityNotifier mUserActivityNotifier; private NumPadKey[] mButtons = new NumPadKey[]{}; private KeyguardPinBasedInputViewController mKeyguardPinViewController; @@ -117,7 +119,7 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener, mEmergencyButtonController, mFalsingCollector, featureFlags, - mSelectedUserInteractor, keyguardKeyboardInteractor) { + mSelectedUserInteractor, keyguardKeyboardInteractor, null, mUserActivityNotifier) { @Override public void onResume(int reason) { super.onResume(reason); diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index ca585abf5100..18ed22a3f4a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -154,6 +154,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var postureController: DevicePostureController @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier @Captor private lateinit var swipeListenerArgumentCaptor: @@ -239,6 +240,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { featureFlags, mSelectedUserInteractor, keyguardKeyboardInteractor, + null, + mUserActivityNotifier ) kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt index 460461a003f6..176c3ac43936 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt @@ -22,10 +22,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -38,13 +39,15 @@ import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidJUnit4::class) class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val secureSettings = kosmos.fakeSettings + @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() // mocks @Mock private lateinit var a11yManager: AccessibilityManager - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - private val secureSettings = FakeSettings() private val userA11yQsShortcutsRepositoryFactory = object : UserA11yQsShortcutsRepository.Factory { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt index 4e1f82c24bb6..801d3599ac10 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt @@ -23,11 +23,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -41,9 +42,10 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! private val testUser2 = UserHandle.of(2)!! - private val testDispatcher = StandardTestDispatcher() - private val scope = TestScope(testDispatcher) - private val settings: FakeSettings = FakeSettings() + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val scope = kosmos.testScope + private val settings = kosmos.fakeSettings private lateinit var underTest: ColorCorrectionRepository diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt index b99dec44b519..2f457be8a81b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt @@ -23,11 +23,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -41,9 +42,10 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! private val testUser2 = UserHandle.of(2)!! - private val testDispatcher = StandardTestDispatcher() - private val scope = TestScope(testDispatcher) - private val settings: FakeSettings = FakeSettings() + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val scope = kosmos.testScope + private val settings = kosmos.fakeSettings private lateinit var underTest: ColorInversionRepository diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt index 5757f67cc1dd..54dbed8407d0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt @@ -26,7 +26,9 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dagger.NightDisplayListenerModule -import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.android.systemui.user.utils.UserScopedService import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq @@ -38,8 +40,6 @@ import com.android.systemui.utils.leaks.FakeLocationController import com.google.common.truth.Truth.assertThat import java.time.LocalTime import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -51,7 +51,7 @@ import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) class NightDisplayRepositoryTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val testUser = UserHandle.of(1)!! private val testStartTime = LocalTime.MIDNIGHT private val testEndTime = LocalTime.NOON @@ -71,8 +71,8 @@ class NightDisplayRepositoryTest : SysuiTestCase() { } private val globalSettings = kosmos.fakeGlobalSettings private val secureSettings = kosmos.fakeSettings - private val testDispatcher = StandardTestDispatcher() - private val scope = TestScope(testDispatcher) + private val testDispatcher = kosmos.testDispatcher + private val scope = kosmos.testScope private val userScopedColorDisplayManager = mock<UserScopedService<ColorDisplayManager>> { whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt index 1378dac98eaa..729d356e2be1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/OneHandedModeRepositoryImplTest.kt @@ -22,11 +22,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -39,9 +40,10 @@ class OneHandedModeRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! private val testUser2 = UserHandle.of(2)!! - private val testDispatcher = StandardTestDispatcher() - private val scope = TestScope(testDispatcher) - private val settings: FakeSettings = FakeSettings() + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val scope = kosmos.testScope + private val settings = kosmos.fakeSettings private val underTest: OneHandedModeRepository = OneHandedModeRepositoryImpl( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt index ce22e288e292..62f13f8e38e6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt @@ -21,10 +21,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -32,9 +33,10 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() { - private val secureSettings = FakeSettings() - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val secureSettings = kosmos.fakeSettings private val underTest = UserA11yQsShortcutsRepository( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index d7acaaf796f8..80de087971c5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -33,7 +33,7 @@ import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import org.junit.Before; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index 4373c880d999..46f076a75116 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -46,7 +46,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.MotionEventHelper; import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.DismissView; import org.junit.After; import org.junit.Before; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt index c1615253804c..8c8faee99139 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt @@ -19,9 +19,11 @@ package com.android.systemui.bouncer.ui.viewmodel import android.content.pm.UserInfo import android.hardware.biometrics.BiometricFaceConstants import android.hardware.fingerprint.FingerprintManager +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository @@ -35,7 +37,6 @@ import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepositor import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.domain.interactor.bouncerInteractor -import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus @@ -71,6 +72,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@EnableFlags(Flags.FLAG_COMPOSE_BOUNCER) class BouncerMessageViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -82,7 +84,6 @@ class BouncerMessageViewModelTest : SysuiTestCase() { @Before fun setUp() { kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) - kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true overrideResource( R.array.config_face_acquire_device_entry_ignorelist, intArrayOf(ignoreHelpMessageId) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt index c37b33e52fa6..ae1c496797c2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt @@ -29,7 +29,7 @@ import com.android.systemui.log.logcatLogBuffer import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before @@ -45,8 +45,7 @@ class CommunalTutorialRepositoryImplTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - - private lateinit var secureSettings: FakeSettings + private val secureSettings = kosmos.fakeSettings private lateinit var userRepository: FakeUserRepository private lateinit var underTest: CommunalTutorialRepositoryImpl @@ -55,7 +54,6 @@ class CommunalTutorialRepositoryImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - secureSettings = FakeSettings() userRepository = FakeUserRepository() val listOfUserInfo = listOf(MAIN_USER_INFO) userRepository.setUserInfos(listOfUserInfo) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 99fcbb854e3a..777ddab4e259 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -68,15 +68,16 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.statusbar.phone.fakeManagedProfileController import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever +import com.android.systemui.utils.leaks.FakeManagedProfileController import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -122,6 +123,7 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var userTracker: FakeUserTracker private lateinit var activityStarter: ActivityStarter private lateinit var userManager: UserManager + private lateinit var managedProfileController: FakeManagedProfileController private lateinit var underTest: CommunalInteractor @@ -143,6 +145,7 @@ class CommunalInteractorTest : SysuiTestCase() { userTracker = kosmos.fakeUserTracker activityStarter = kosmos.activityStarter userManager = kosmos.userManager + managedProfileController = kosmos.fakeManagedProfileController whenever(mainUser.isMain).thenReturn(true) whenever(secondaryUser.isMain).thenReturn(false) @@ -1070,6 +1073,14 @@ class CommunalInteractorTest : SysuiTestCase() { } } + @Test + fun unpauseWorkProfileEnablesWorkMode() = + testScope.runTest { + underTest.unpauseWorkProfile() + + assertThat(managedProfileController.isWorkModeEnabled()).isTrue() + } + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 3e75cebb1c7d..d4a76910c46a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -96,6 +96,7 @@ import java.io.PrintWriter import java.io.StringWriter import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent @@ -561,14 +562,29 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun withSceneContainerEnabled_authenticateDoesNotRunWhenKeyguardIsGoingAway() = testScope.runTest { testGatingCheckForFaceAuth(sceneContainerEnabled = true) { - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - KeyguardState.LOCKSCREEN, - KeyguardState.UNDEFINED, - value = 0.5f, - transitionState = TransitionState.RUNNING - ), - validateStep = false + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = Scenes.Bouncer, + toScene = Scenes.Gone, + currentScene = flowOf(Scenes.Bouncer), + progress = MutableStateFlow(0.2f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + ) + ) + ) + runCurrent() + } + } + + @Test + @EnableSceneContainer + fun withSceneContainerEnabled_authenticateDoesNotRunWhenLockscreenIsGone() = + testScope.runTest { + testGatingCheckForFaceAuth(sceneContainerEnabled = true) { + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow(ObservableTransitionState.Idle(Scenes.Gone)) ) runCurrent() } @@ -898,15 +914,32 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun withSceneContainer_faceDetectDoesNotRunWhenKeyguardGoingAway() = testScope.runTest { testGatingCheckForDetect(sceneContainerEnabled = true) { - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - KeyguardState.LOCKSCREEN, - KeyguardState.UNDEFINED, - value = 0.5f, - transitionState = TransitionState.RUNNING - ), - validateStep = false + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow( + ObservableTransitionState.Transition( + fromScene = Scenes.Bouncer, + toScene = Scenes.Gone, + currentScene = flowOf(Scenes.Bouncer), + progress = MutableStateFlow(0.2f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + ) + ) + ) + + runCurrent() + } + } + + @Test + @EnableSceneContainer + fun withSceneContainer_faceDetectDoesNotRunWhenLockscreenIsGone() = + testScope.runTest { + testGatingCheckForDetect(sceneContainerEnabled = true) { + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow(ObservableTransitionState.Idle(Scenes.Gone)) ) + runCurrent() } } @@ -1231,6 +1264,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { TransitionStep(KeyguardState.OFF, KeyguardState.LOCKSCREEN, value = 1.0f), validateStep = false ) + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow(ObservableTransitionState.Idle(Scenes.Lockscreen)) + ) } else { keyguardRepository.setKeyguardGoingAway(false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index d21a8270ae54..5e6ff73e5e5e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -1139,6 +1139,39 @@ class DreamOverlayServiceTest : SysuiTestCase() { } @Test + fun testDreamActivityGesturesNotBlockedWhenNotificationShadeShowing() { + val client = client + + // Inform the overlay service of dream starting. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*isPreview*/, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + + val matcherCaptor = argumentCaptor<TaskMatcher>() + verify(gestureInteractor) + .addGestureBlockedMatcher(matcherCaptor.capture(), eq(GestureInteractor.Scope.Global)) + val matcher = matcherCaptor.firstValue + + val dreamTaskInfo = TaskInfo(mock<ComponentName>(), WindowConfiguration.ACTIVITY_TYPE_DREAM) + assertThat(matcher.matches(dreamTaskInfo)).isTrue() + + val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture()) + + // Notification shade opens. + callbackCaptor.value.onShadeExpandedChanged(true) + mMainExecutor.runAllReady() + + verify(gestureInteractor) + .removeGestureBlockedMatcher(eq(matcher), eq(GestureInteractor.Scope.Global)) + } + + @Test fun testComponentsRecreatedBetweenDreams() { clearInvocations( mDreamComplicationComponentFactory, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index bbfaf6fa4ce6..6c3c7ef0162d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -31,19 +31,19 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.ZenModeController +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -63,27 +63,24 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val settings = kosmos.fakeSettings + @Mock private lateinit var zenModeController: ZenModeController @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var conditionUri: Uri @Mock private lateinit var enableZenModeDialog: EnableZenModeDialog @Captor private lateinit var spyZenMode: ArgumentCaptor<Int> @Captor private lateinit var spyConditionId: ArgumentCaptor<Uri?> - private lateinit var settings: FakeSettings private lateinit var underTest: DoNotDisturbQuickAffordanceConfig - private lateinit var testDispatcher: TestDispatcher - private lateinit var testScope: TestScope @Before fun setUp() { MockitoAnnotations.initMocks(this) - testDispatcher = StandardTestDispatcher() - testScope = TestScope(testDispatcher) - - settings = FakeSettings() - underTest = DoNotDisturbQuickAffordanceConfig( context, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt index 26fcb234843d..0145f1748b10 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -23,19 +23,19 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.unconfinedTestDispatcher +import com.android.systemui.kosmos.unconfinedTestScope import com.android.systemui.res.R import com.android.systemui.settings.FakeUserTracker import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before @@ -51,14 +51,15 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.unconfinedTestDispatcher + private val testScope = kosmos.unconfinedTestScope + private val settings = kosmos.unconfinedDispatcherFakeSettings + @Mock private lateinit var sharedPrefs: FakeSharedPreferences private lateinit var underTest: KeyguardQuickAffordanceLegacySettingSyncer - - private lateinit var testScope: TestScope - private lateinit var testDispatcher: TestDispatcher private lateinit var selectionManager: KeyguardQuickAffordanceLocalUserSelectionManager - private lateinit var settings: FakeSettings @Before fun setUp() { @@ -73,8 +74,6 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { whenever(resources.getBoolean(R.bool.custom_lockscreen_shortcuts_enabled)).thenReturn(true) whenever(context.resources).thenReturn(resources) - testDispatcher = UnconfinedTestDispatcher() - testScope = TestScope(testDispatcher) selectionManager = KeyguardQuickAffordanceLocalUserSelectionManager( context = context, @@ -92,7 +91,6 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { userTracker = FakeUserTracker(), broadcastDispatcher = fakeBroadcastDispatcher, ) - settings = FakeSettings() settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0) settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_WALLET, 0) settings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER, 0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index c85cd662dac6..1582e4776e15 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -31,20 +31,21 @@ import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanc import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.UserFileManager import com.android.systemui.shared.customization.data.content.FakeCustomizationProviderClient import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import java.util.Locale import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before @@ -58,6 +59,11 @@ import org.mockito.ArgumentMatchers.anyString @RunWith(AndroidJUnit4::class) class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val settings = kosmos.fakeSettings + private lateinit var underTest: KeyguardQuickAffordanceRepository private lateinit var config1: FakeKeyguardQuickAffordanceConfig @@ -65,7 +71,6 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { private lateinit var userTracker: FakeUserTracker private lateinit var client1: FakeCustomizationProviderClient private lateinit var client2: FakeCustomizationProviderClient - private lateinit var testScope: TestScope @Before fun setUp() { @@ -73,8 +78,6 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { context.resources.configuration.setLayoutDirection(Locale.US) config1 = FakeKeyguardQuickAffordanceConfig(FakeCustomizationProviderClient.AFFORDANCE_1) config2 = FakeKeyguardQuickAffordanceConfig(FakeCustomizationProviderClient.AFFORDANCE_2) - val testDispatcher = StandardTestDispatcher() - testScope = TestScope(testDispatcher) userTracker = FakeUserTracker() val localUserSelectionManager = KeyguardQuickAffordanceLocalUserSelectionManager( @@ -128,7 +131,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { KeyguardQuickAffordanceLegacySettingSyncer( scope = testScope.backgroundScope, backgroundDispatcher = testDispatcher, - secureSettings = FakeSettings(), + secureSettings = settings, selectionsManager = localUserSelectionManager, ), configs = setOf(config1, config2), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index ad07c1c1b88f..a8bb2b0aca56 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -61,7 +61,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -80,6 +80,10 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val settings = kosmos.fakeSettings + @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var userTracker: UserTracker @@ -90,11 +94,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Mock private lateinit var logger: KeyguardQuickAffordancesLogger @Mock private lateinit var metricsLogger: KeyguardQuickAffordancesMetricsLogger - private val kosmos = testKosmos() - private lateinit var underTest: KeyguardQuickAffordanceInteractor - private val testScope = kosmos.testScope private lateinit var repository: FakeKeyguardRepository private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig @@ -170,7 +171,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { KeyguardQuickAffordanceLegacySettingSyncer( scope = testScope.backgroundScope, backgroundDispatcher = kosmos.testDispatcher, - secureSettings = FakeSettings(), + secureSettings = settings, selectionsManager = localUserSelectionManager, ), configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 6708ffa2a091..12039c135985 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -1394,9 +1394,11 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { kosmos.setSceneTransition(Transition(Scenes.Lockscreen, Scenes.Gone)) val sendStep1 = TransitionStep(LOCKSCREEN, UNDEFINED, 0f, STARTED) + sendSteps(sendStep1) + kosmos.setSceneTransition(Idle(Scenes.Gone)) val sendStep2 = TransitionStep(LOCKSCREEN, UNDEFINED, 1f, FINISHED) val sendStep3 = TransitionStep(LOCKSCREEN, AOD, 0f, STARTED) - sendSteps(sendStep1, sendStep2, sendStep3) + sendSteps(sendStep2, sendStep3) assertEquals(listOf(sendStep1, sendStep2), currentStates) assertEquals(listOf(sendStep1, sendStep2), currentStatesConverted) @@ -1410,6 +1412,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Lockscreen)) val sendStep1 = TransitionStep(LOCKSCREEN, UNDEFINED, 0f, STARTED) + kosmos.setSceneTransition(Idle(Scenes.Lockscreen)) val sendStep2 = TransitionStep(LOCKSCREEN, UNDEFINED, 1f, FINISHED) val sendStep3 = TransitionStep(LOCKSCREEN, AOD, 0f, STARTED) sendSteps(sendStep1, sendStep2, sendStep3) @@ -1426,6 +1429,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Lockscreen)) val sendStep1 = TransitionStep(LOCKSCREEN, DOZING, 0f, STARTED) + kosmos.setSceneTransition(Idle(Scenes.Lockscreen)) val sendStep2 = TransitionStep(LOCKSCREEN, DOZING, 1f, FINISHED) val sendStep3 = TransitionStep(LOCKSCREEN, AOD, 0f, STARTED) sendSteps(sendStep1, sendStep2, sendStep3) @@ -1443,6 +1447,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Lockscreen)) val sendStep1 = TransitionStep(LOCKSCREEN, DOZING, 0f, STARTED) + kosmos.setSceneTransition(Idle(Scenes.Lockscreen)) val sendStep2 = TransitionStep(LOCKSCREEN, DOZING, 1f, FINISHED) val sendStep3 = TransitionStep(LOCKSCREEN, AOD, 0f, STARTED) val sendStep4 = TransitionStep(AOD, LOCKSCREEN, 0f, STARTED) @@ -1461,10 +1466,12 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { kosmos.setSceneTransition(Transition(Scenes.Lockscreen, Scenes.Gone)) val sendStep1 = TransitionStep(LOCKSCREEN, UNDEFINED, 0f, STARTED) + sendSteps(sendStep1) + kosmos.setSceneTransition(Idle(Scenes.Gone)) val sendStep2 = TransitionStep(LOCKSCREEN, UNDEFINED, 1f, FINISHED) val sendStep3 = TransitionStep(UNDEFINED, AOD, 0f, STARTED) val sendStep4 = TransitionStep(AOD, LOCKSCREEN, 0f, STARTED) - sendSteps(sendStep1, sendStep2, sendStep3, sendStep4) + sendSteps(sendStep2, sendStep3, sendStep4) assertEquals(listOf(sendStep1, sendStep2), currentStates) assertEquals(listOf(sendStep1, sendStep2), currentStatesMapped) @@ -1478,10 +1485,12 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Lockscreen)) val sendStep1 = TransitionStep(LOCKSCREEN, UNDEFINED, 0f, STARTED) + sendSteps(sendStep1) + kosmos.setSceneTransition(Idle(Scenes.Gone)) val sendStep2 = TransitionStep(LOCKSCREEN, UNDEFINED, 1f, FINISHED) val sendStep3 = TransitionStep(UNDEFINED, AOD, 0f, STARTED) val sendStep4 = TransitionStep(AOD, LOCKSCREEN, 0f, STARTED) - sendSteps(sendStep1, sendStep2, sendStep3, sendStep4) + sendSteps(sendStep2, sendStep3, sendStep4) assertEquals(listOf<TransitionStep>(), currentStatesMapped) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 3e1f4f6da5e4..3b2b12c4363d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -360,6 +360,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test + @DisableSceneContainer fun alpha_transitionBetweenHubAndDream_isZero() = testScope.runTest { val alpha by collectLastValue(underTest.alpha(viewState)) @@ -388,8 +389,8 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() ObservableTransitionState.Transition( fromScene = Scenes.Lockscreen, toScene = Scenes.Communal, - emptyFlow(), - emptyFlow(), + flowOf(Scenes.Communal), + flowOf(0.5f), false, emptyFlow() ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt new file mode 100644 index 000000000000..f6865f137071 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModelTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 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.notifications.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest = kosmos.notificationsShadeOverlayActionsViewModel + + @Test + fun upTransitionSceneKey_hidesShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + underTest.activateIn(this) + + assertThat((actions?.get(Swipe.Up) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.NotificationsShade) + assertThat(actions?.get(Swipe.Down)).isNull() + } + + @Test + fun back_hidesShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + underTest.activateIn(this) + + assertThat((actions?.get(Back) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.NotificationsShade) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt new file mode 100644 index 000000000000..88a1df147489 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 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.notifications.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + + private val underTest = kosmos.notificationsShadeOverlayContentViewModel + + @Test + fun onScrimClicked_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + sceneInteractor.showOverlay( + overlay = Overlays.NotificationsShade, + loggingReason = "test", + ) + assertThat(currentOverlays).contains(Overlays.NotificationsShade) + + underTest.onScrimClicked() + + assertThat(currentOverlays).doesNotContain(Overlays.NotificationsShade) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt index 0505e1996927..ed7f96fb2b66 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneActionsViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -104,36 +103,6 @@ class NotificationsShadeSceneActionsViewModelTest : SysuiTestCase() { } @Test - fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() = - testScope.runTest { - kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - val actions by collectLastValue(underTest.actions) - lockDevice() - underTest.activateIn(this) - - assertThat((actions?.get(Swipe.Down) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(actions?.get(Swipe.Up)).isNull() - assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value) - .isEqualTo(Scenes.Lockscreen) - } - - @Test - fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() = - testScope.runTest { - kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - val actions by collectLastValue(underTest.actions) - lockDevice() - unlockDevice() - underTest.activateIn(this) - - assertThat((actions?.get(Swipe.Down) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(actions?.get(Swipe.Up)).isNull() - assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) - } - - @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { val actions by collectLastValue(underTest.actions) @@ -153,11 +122,13 @@ class NotificationsShadeSceneActionsViewModelTest : SysuiTestCase() { @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { + val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) val actions by collectLastValue(underTest.actions) kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.None ) + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() sceneInteractor // force the lazy; this will kick off StateFlows runCurrent() sceneInteractor.changeScene(Scenes.Gone, "reason") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt index d153e9d1d361..561902234990 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/AutoAddableSettingTest.kt @@ -21,14 +21,15 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking import com.android.systemui.qs.pipeline.shared.TileSpec -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -38,10 +39,11 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AutoAddableSettingTest : SysuiTestCase() { - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val secureSettings = kosmos.fakeSettings - private val secureSettings = FakeSettings() private val underTest = AutoAddableSetting( secureSettings, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt index 5fd3a242e195..a18f450cdc9e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -209,7 +209,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() { private companion object { val TEST_USER = UserHandle.of(1)!! - val MODES_DRAWABLE_ID = com.android.systemui.res.R.drawable.qs_dnd_icon_off + val MODES_DRAWABLE_ID = R.drawable.ic_zen_priority_modes val MODES_DRAWABLE = TestStubDrawable("modes_icon") val BEDTIME_DRAWABLE = TestStubDrawable("bedtime") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt index e2149d907688..424afe171f96 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.ui.viewmodel +import android.platform.test.annotations.DisableFlags import android.testing.TestableLooper.RunWithLooper import androidx.lifecycle.LifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,20 +27,26 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.sceneContainerStartable +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -51,6 +58,7 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) @RunWithLooper @EnableSceneContainer +@DisableFlags(com.android.systemui.Flags.FLAG_DUAL_SHADE) class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -64,6 +72,8 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { private val footerActionsController = mock<FooterActionsController>() private val sceneContainerStartable = kosmos.sceneContainerStartable + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val shadeInteractor by lazy { kosmos.shadeInteractor } private lateinit var underTest: QuickSettingsSceneContentViewModel @@ -80,7 +90,10 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { footerActionsViewModelFactory = footerActionsViewModelFactory, footerActionsController = footerActionsController, mediaCarouselInteractor = kosmos.mediaCarouselInteractor, + shadeInteractor = shadeInteractor, + sceneInteractor = sceneInteractor, ) + underTest.activateIn(testScope) } @Test @@ -122,4 +135,16 @@ class QuickSettingsSceneContentViewModelTest : SysuiTestCase() { assertThat(isMediaVisible).isTrue() } + + @Test + fun shadeModeChange_switchToShadeScene() = + testScope.runTest { + val scene by collectLastValue(sceneInteractor.currentScene) + + // switch to split shade + kosmos.shadeRepository.setShadeLayoutWide(true) + runCurrent() + + assertThat(scene).isEqualTo(Scenes.Shade) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt new file mode 100644 index 000000000000..762941d70389 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 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.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest = kosmos.quickSettingsShadeOverlayActionsViewModel + + @Test + fun upTransitionSceneKey_hidesShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + underTest.activateIn(this) + + assertThat((actions?.get(Swipe.Up) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.QuickSettingsShade) + assertThat(actions?.get(Swipe.Down)).isNull() + } + + @Test + fun back_hidesShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + underTest.activateIn(this) + + assertThat((actions?.get(Back) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.QuickSettingsShade) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt new file mode 100644 index 000000000000..abd1e2c7df82 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 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.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + + private val underTest = kosmos.quickSettingsShadeOverlayContentViewModel + + @Test + fun onScrimClicked_hidesShade() = + testScope.runTest { + val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) + sceneInteractor.showOverlay( + overlay = Overlays.QuickSettingsShade, + loggingReason = "test", + ) + assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) + + underTest.onScrimClicked() + + assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt index db58c8500768..ba527d7ad2b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt @@ -39,7 +39,6 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -107,37 +106,6 @@ class QuickSettingsShadeSceneActionsViewModelTest : SysuiTestCase() { } @Test - fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() = - testScope.runTest { - kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - underTest.activateIn(this) - val actions by collectLastValue(underTest.actions) - val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) - lockDevice() - - assertThat((actions?.get(Swipe.Down) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(actions?.get(Swipe.Up)).isNull() - assertThat(homeScene).isEqualTo(Scenes.Lockscreen) - } - - @Test - fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() = - testScope.runTest { - kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true) - underTest.activateIn(this) - val actions by collectLastValue(underTest.actions) - val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene) - lockDevice() - unlockDevice() - - assertThat((actions?.get(Swipe.Down) as? UserActionResult.ChangeScene)?.toScene) - .isEqualTo(SceneFamilies.Home) - assertThat(actions?.get(Swipe.Up)).isNull() - assertThat(homeScene).isEqualTo(Scenes.Gone) - } - - @Test fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { underTest.activateIn(this) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt deleted file mode 100644 index 3f087b48f509..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2024 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.shade.ui.viewmodel - -import android.testing.TestableLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository -import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor -import com.android.systemui.flags.EnableSceneContainer -import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus -import com.android.systemui.kosmos.testScope -import com.android.systemui.lifecycle.activateIn -import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper -@EnableSceneContainer -class OverlayShadeViewModelTest : SysuiTestCase() { - - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor - private val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } - - private val underTest = kosmos.overlayShadeViewModel - - @Before - fun setUp() { - underTest.activateIn(testScope) - } - - @Test - fun backgroundScene_deviceLocked_lockscreen() = - testScope.runTest { - val backgroundScene by collectLastValue(underTest.backgroundScene) - - lockDevice() - - assertThat(backgroundScene).isEqualTo(Scenes.Lockscreen) - } - - @Test - fun backgroundScene_deviceUnlocked_gone() = - testScope.runTest { - val backgroundScene by collectLastValue(underTest.backgroundScene) - - lockDevice() - unlockDevice() - - assertThat(backgroundScene).isEqualTo(Scenes.Gone) - } - - @Test - fun backgroundScene_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = - testScope.runTest { - val backgroundScene by collectLastValue(underTest.backgroundScene) - val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) - - kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.None - ) - assertThat(deviceUnlockStatus?.isUnlocked).isTrue() - sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - runCurrent() - - assertThat(backgroundScene).isEqualTo(Scenes.Lockscreen) - } - - @Test - fun backgroundScene_authMethodSwipe_lockscreenDismissed_goesToGone() = - testScope.runTest { - val backgroundScene by collectLastValue(underTest.backgroundScene) - val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) - - kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.None - ) - assertThat(deviceUnlockStatus?.isUnlocked).isTrue() - sceneInteractor.changeScene(Scenes.Gone, "reason") - runCurrent() - - assertThat(backgroundScene).isEqualTo(Scenes.Gone) - } - - @Test - fun onScrimClicked_onLockscreen_goesToLockscreen() = - testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene) - lockDevice() - sceneInteractor.changeScene(Scenes.Bouncer, "reason") - runCurrent() - assertThat(currentScene).isNotEqualTo(Scenes.Lockscreen) - - underTest.onScrimClicked() - - assertThat(currentScene).isEqualTo(Scenes.Lockscreen) - } - - @Test - fun onScrimClicked_deviceWasEntered_goesToGone() = - testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene) - val backgroundScene by collectLastValue(underTest.backgroundScene) - - lockDevice() - unlockDevice() - sceneInteractor.changeScene(Scenes.QuickSettings, "reason") - runCurrent() - assertThat(backgroundScene).isEqualTo(Scenes.Gone) - assertThat(currentScene).isNotEqualTo(Scenes.Gone) - - underTest.onScrimClicked() - - assertThat(currentScene).isEqualTo(Scenes.Gone) - } - - private fun TestScope.lockDevice() { - val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) - - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - assertThat(deviceUnlockStatus?.isUnlocked).isFalse() - sceneInteractor.changeScene(Scenes.Lockscreen, "reason") - runCurrent() - } - - private fun TestScope.unlockDevice() { - val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) - - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - assertThat(deviceUnlockStatus?.isUnlocked).isTrue() - sceneInteractor.changeScene(Scenes.Gone, "reason") - runCurrent() - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt index 0f56d0bcc7eb..fa7f37c7ba16 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt @@ -90,8 +90,9 @@ class MediaOutputComponentInteractorTest : SysuiTestCase() { assertThat(model) .isEqualTo( MediaOutputComponentModel.Calling( - AudioOutputDevice.BuiltIn(builtInDeviceName, testIcon), - false, + device = AudioOutputDevice.BuiltIn(builtInDeviceName, testIcon), + isInAudioSharing = false, + canOpenAudioSwitcher = false, ) ) } @@ -101,6 +102,9 @@ class MediaOutputComponentInteractorTest : SysuiTestCase() { fun hasSession_stateIs_MediaSession() = with(kosmos) { testScope.runTest { + localMediaRepository.updateCurrentConnectedDevice( + TestMediaDevicesFactory.builtInMediaDevice() + ) mediaControllerRepository.setActiveSessions(listOf(localMediaController)) val model by collectLastValue(underTest.mediaOutputModel.filterData()) @@ -113,6 +117,7 @@ class MediaOutputComponentInteractorTest : SysuiTestCase() { assertThat(device) .isEqualTo(AudioOutputDevice.BuiltIn("built_in_media", testIcon)) assertThat(isInAudioSharing).isFalse() + assertThat(canOpenAudioSwitcher).isTrue() } } } @@ -129,8 +134,9 @@ class MediaOutputComponentInteractorTest : SysuiTestCase() { assertThat(model) .isEqualTo( MediaOutputComponentModel.Idle( - AudioOutputDevice.BuiltIn("built_in_media", testIcon), - true, + device = AudioOutputDevice.BuiltIn("built_in_media", testIcon), + isInAudioSharing = true, + canOpenAudioSwitcher = false, ) ) } diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index e8fd2ef6eafa..38ef0e9d5df4 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1052,9 +1052,6 @@ <!-- The width of the shortcut helper container, as a fraction of the screen's width. --> <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">1.0</item> - <!-- Only applicable for dual shade - Allow Notifications/QS shade to anchor to the bottom. --> - <bool name="config_dualShadeAlignedToBottom">false</bool> - <!-- List of packages for which we want to use activity info (instead of application info) for biometric prompt logo. Empty for AOSP. [DO NOT TRANSLATE] --> <string-array name="config_useActivityLogoForBiometricPrompt" translatable="false"/> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index fdba2e67664f..d3d757bcdb46 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -721,8 +721,8 @@ <!-- QuickSettings: Do not disturb - Priority only [CHAR LIMIT=NONE] --> <!-- QuickSettings: Do not disturb - Alarms only [CHAR LIMIT=NONE] --> <!-- QuickSettings: Do not disturb - Total silence [CHAR LIMIT=NONE] --> - <!-- QuickSettings: Priority modes [CHAR LIMIT=NONE] --> - <string name="quick_settings_modes_label">Priority modes</string> + <!-- QuickSettings: Modes [CHAR LIMIT=NONE] --> + <string name="quick_settings_modes_label">Modes</string> <!-- QuickSettings: Bluetooth [CHAR LIMIT=NONE] --> <string name="quick_settings_bluetooth_label">Bluetooth</string> <!-- QuickSettings: Bluetooth (Multiple) [CHAR LIMIT=NONE] --> @@ -1097,28 +1097,28 @@ <!-- QuickStep: Accessibility to toggle overview [CHAR LIMIT=40] --> <string name="quick_step_accessibility_toggle_overview">Toggle Overview</string> - <!-- Priority modes dialog title [CHAR LIMIT=35] --> - <string name="zen_modes_dialog_title">Priority modes</string> + <!-- Modes dialog title [CHAR LIMIT=35] --> + <string name="zen_modes_dialog_title">Modes</string> - <!-- Priority modes dialog confirmation button [CHAR LIMIT=15] --> + <!-- Modes dialog confirmation button [CHAR LIMIT=15] --> <string name="zen_modes_dialog_done">Done</string> - <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] --> + <!-- Modes dialog settings shortcut button [CHAR LIMIT=15] --> <string name="zen_modes_dialog_settings">Settings</string> - <!-- Priority modes: label for an active mode [CHAR LIMIT=35] --> + <!-- Modes: label for an active mode [CHAR LIMIT=35] --> <string name="zen_mode_on">On</string> - <!-- Priority modes: label for an active mode, with details [CHAR LIMIT=10] --> + <!-- Modes: label for an active mode, with details [CHAR LIMIT=10] --> <string name="zen_mode_on_with_details">On • <xliff:g id="trigger_description" example="Mon-Fri, 23:00-7:00">%1$s</xliff:g></string> - <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] --> + <!-- Modes: label for an inactive mode [CHAR LIMIT=35] --> <string name="zen_mode_off">Off</string> - <!-- Priority modes: label for a mode that needs to be set up [CHAR LIMIT=35] --> + <!-- Modes: label for a mode that needs to be set up [CHAR LIMIT=35] --> <string name="zen_mode_set_up">Set up</string> - <!-- Priority modes: label for a mode that cannot be manually turned on [CHAR LIMIT=35] --> + <!-- Modes: label for a mode that cannot be manually turned on [CHAR LIMIT=35] --> <string name="zen_mode_no_manual_invocation">Manage in settings</string> <string name="zen_mode_active_modes"> @@ -1380,9 +1380,13 @@ <string name="media_projection_entry_app_permission_dialog_title">Share your screen with <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>?</string> <!-- 1P/3P app media projection permission option for capturing just a single app [CHAR LIMIT=50] --> - <string name="media_projection_entry_app_permission_dialog_option_text_single_app">Share one app</string> + <string name="screen_share_permission_dialog_option_single_app">Share one app</string> + <!-- CTS tests rely on the `screen_share_permission_dialog_option_single_app` resource name, so just point the updated resource name to the old resource name. --> + <string name="media_projection_entry_app_permission_dialog_option_text_single_app">@string/screen_share_permission_dialog_option_single_app</string> <!-- 1P/3P app media projection permission option for capturing the whole screen [CHAR LIMIT=50] --> - <string name="media_projection_entry_app_permission_dialog_option_text_entire_screen">Share entire screen</string> + <string name="screen_share_permission_dialog_option_entire_screen">Share entire screen</string> + <!-- CTS tests rely on the `screen_share_permission_dialog_option_entire_screen` resource name, so just point the updated resource name to the old resource name. --> + <string name="media_projection_entry_app_permission_dialog_option_text_entire_screen">@string/screen_share_permission_dialog_option_entire_screen</string> <!-- 1P/3P app media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] --> <string name="media_projection_entry_app_permission_dialog_warning_entire_screen">When you’re sharing your entire screen, anything on your screen is visible to <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string> <!-- 1P/3P app media projection permission warning for capturing an app. [CHAR LIMIT=350] --> diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp index e68da09b26d1..8f55961af4e9 100644 --- a/packages/SystemUI/shared/Android.bp +++ b/packages/SystemUI/shared/Android.bp @@ -49,7 +49,6 @@ android_library { "src/**/*.aidl", ":wm_shell-aidls", ":wm_shell-shared-aidls", - ":wm_shell_util-sources", ], static_libs: [ "BiometricsSharedLib", diff --git a/packages/SystemUI/src/com/android/keyguard/AuthInteractionProperties.kt b/packages/SystemUI/src/com/android/keyguard/AuthInteractionProperties.kt new file mode 100644 index 000000000000..efa13c632087 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/AuthInteractionProperties.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.keyguard + +import android.os.VibrationAttributes +import com.google.android.msdl.domain.InteractionProperties + +/** + * This class represents the set of [InteractionProperties] that only hold [VibrationAttributes] for + * the case of user authentication. + */ +data class AuthInteractionProperties( + override val vibrationAttributes: VibrationAttributes = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST) +) : InteractionProperties diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index dad440083f70..28f1381d94af 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -19,7 +19,10 @@ package com.android.keyguard; import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL; import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED; import static com.android.keyguard.KeyguardAbsKeyInputView.MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT; +import static com.android.systemui.Flags.msdlFeedback; +import static com.android.systemui.Flags.notifyPasswordTextViewUserActivityInBackground; +import android.annotation.Nullable; import android.content.res.ColorStateList; import android.os.AsyncTask; import android.os.CountDownTimer; @@ -40,6 +43,9 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.data.model.MSDLToken; +import com.google.android.msdl.domain.MSDLPlayer; + import java.util.HashMap; import java.util.Map; @@ -50,11 +56,14 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey private final LatencyTracker mLatencyTracker; private final FalsingCollector mFalsingCollector; private final EmergencyButtonController mEmergencyButtonController; + private final UserActivityNotifier mUserActivityNotifier; private CountDownTimer mCountdownTimer; private boolean mDismissing; protected AsyncTask<?, ?, ?> mPendingLockCheck; protected boolean mResumed; protected boolean mLockedOut; + @Nullable + protected MSDLPlayer mMSDLPlayer; private final KeyDownListener mKeyDownListener = (keyCode, keyEvent) -> { // Fingerprint sensor sends a KeyEvent.KEYCODE_UNKNOWN. @@ -81,7 +90,9 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey KeyguardMessageAreaController.Factory messageAreaControllerFactory, LatencyTracker latencyTracker, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, - FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor) { + FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, + @Nullable MSDLPlayer msdlPlayer, + UserActivityNotifier userActivityNotifier) { super(view, securityMode, keyguardSecurityCallback, emergencyButtonController, messageAreaControllerFactory, featureFlags, selectedUserInteractor); mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -89,6 +100,8 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey mLatencyTracker = latencyTracker; mFalsingCollector = falsingCollector; mEmergencyButtonController = emergencyButtonController; + mMSDLPlayer = msdlPlayer; + mUserActivityNotifier = userActivityNotifier; } abstract void resetState(); @@ -178,6 +191,7 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey void onPasswordChecked(int userId, boolean matched, int timeoutMs, boolean isValidPassword) { boolean dismissKeyguard = mSelectedUserInteractor.getSelectedUserId() == userId; if (matched) { + playAuthenticationHaptics(/* unlock= */true); getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0); if (dismissKeyguard) { mDismissing = true; @@ -185,6 +199,7 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey getKeyguardSecurityCallback().dismiss(true, userId, getSecurityMode()); } } else { + playAuthenticationHaptics(/* unlock= */false); mView.resetPasswordText(true /* animate */, false /* announce deletion if no match */); if (isValidPassword) { getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs); @@ -201,6 +216,18 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey } } + private void playAuthenticationHaptics(boolean unlock) { + if (!msdlFeedback() || mMSDLPlayer == null) return; + + MSDLToken token; + if (unlock) { + token = MSDLToken.UNLOCK; + } else { + token = MSDLToken.FAILURE; + } + mMSDLPlayer.playToken(token, mAuthInteractionProperties); + } + protected void startErrorAnimation() { /* no-op */ } protected void verifyPasswordAndUnlock() { @@ -280,6 +307,9 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey getKeyguardSecurityCallback().userActivity(); getKeyguardSecurityCallback().onUserInput(); mMessageAreaController.setMessage(""); + if (notifyPasswordTextViewUserActivityInBackground()) { + mUserActivityNotifier.notifyUserActivity(); + } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index db14a0f67fca..dd84bc6989a4 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -45,6 +45,9 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.ViewController; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.google.android.msdl.domain.InteractionProperties; +import com.google.android.msdl.domain.MSDLPlayer; + import javax.inject.Inject; /** Controller for a {@link KeyguardSecurityView}. */ @@ -63,6 +66,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() {}; private final FeatureFlags mFeatureFlags; protected final SelectedUserInteractor mSelectedUserInteractor; + protected final InteractionProperties mAuthInteractionProperties = + new AuthInteractionProperties(); protected KeyguardInputViewController(T view, SecurityMode securityMode, KeyguardSecurityCallback keyguardSecurityCallback, @@ -214,6 +219,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> private final SelectedUserInteractor mSelectedUserInteractor; private final UiEventLogger mUiEventLogger; private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor; + private final MSDLPlayer mMSDLPlayer; + private final UserActivityNotifier mUserActivityNotifier; @Inject public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -228,7 +235,9 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> KeyguardViewController keyguardViewController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, - KeyguardKeyboardInteractor keyguardKeyboardInteractor) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor, + MSDLPlayer msdlPlayer, + UserActivityNotifier userActivityNotifier) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -246,6 +255,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mSelectedUserInteractor = selectedUserInteractor; mUiEventLogger = uiEventLogger; mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; + mMSDLPlayer = msdlPlayer; + mUserActivityNotifier = userActivityNotifier; } /** Create a new {@link KeyguardInputViewController}. */ @@ -268,29 +279,29 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mInputMethodManager, emergencyButtonController, mMainExecutor, mResources, mFalsingCollector, mKeyguardViewController, mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, - mKeyguardKeyboardInteractor); + mKeyguardKeyboardInteractor, mMSDLPlayer, mUserActivityNotifier); } else if (keyguardInputView instanceof KeyguardPINView) { return new KeyguardPinViewController((KeyguardPINView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, emergencyButtonController, mFalsingCollector, mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, - mUiEventLogger, mKeyguardKeyboardInteractor - ); + mUiEventLogger, mKeyguardKeyboardInteractor, mMSDLPlayer, + mUserActivityNotifier); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyguardKeyboardInteractor); + mKeyguardKeyboardInteractor, mMSDLPlayer, mUserActivityNotifier); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, - mKeyguardKeyboardInteractor + mKeyguardKeyboardInteractor, mMSDLPlayer, mUserActivityNotifier ); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index 3ad73bc17704..905fa0939a46 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -19,6 +19,7 @@ package com.android.keyguard; import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import android.annotation.Nullable; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -55,6 +56,8 @@ import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.google.android.msdl.domain.MSDLPlayer; + import java.util.List; public class KeyguardPasswordViewController @@ -134,10 +137,13 @@ public class KeyguardPasswordViewController DevicePostureController postureController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - KeyguardKeyboardInteractor keyguardKeyboardInteractor) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor, + @Nullable MSDLPlayer msdlPlayer, + UserActivityNotifier userActivityNotifier) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, - emergencyButtonController, featureFlags, selectedUserInteractor); + emergencyButtonController, featureFlags, selectedUserInteractor, msdlPlayer, + userActivityNotifier); mKeyguardSecurityCallback = keyguardSecurityCallback; mInputMethodManager = inputMethodManager; mPostureController = postureController; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 0f61233ac64f..f575cf29f402 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -16,9 +16,11 @@ package com.android.keyguard; +import static com.android.systemui.Flags.msdlFeedback; import static com.android.systemui.Flags.pinInputFieldStyledFocusState; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import android.annotation.Nullable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.StateListDrawable; @@ -40,6 +42,9 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.data.model.MSDLToken; +import com.google.android.msdl.domain.MSDLPlayer; + public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinBasedInputView> extends KeyguardAbsKeyInputViewController<T> { @@ -77,10 +82,13 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB FalsingCollector falsingCollector, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - KeyguardKeyboardInteractor keyguardKeyboardInteractor) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor, + @Nullable MSDLPlayer msdlPlayer, + UserActivityNotifier userActivityNotifier) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, - emergencyButtonController, featureFlags, selectedUserInteractor); + emergencyButtonController, featureFlags, selectedUserInteractor, msdlPlayer, + userActivityNotifier); mLiftToActivateListener = liftToActivateListener; mFalsingCollector = falsingCollector; mKeyguardKeyboardInteractor = keyguardKeyboardInteractor; @@ -102,12 +110,22 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB return false; }); button.setAnimationEnabled(showAnimations); + button.setMSDLPlayer(mMSDLPlayer); } mPasswordEntry.setOnKeyListener(mOnKeyListener); mPasswordEntry.setUserActivityListener(this::onUserInput); View deleteButton = mView.findViewById(R.id.delete_button); - deleteButton.setOnTouchListener(mActionButtonTouchListener); + if (msdlFeedback()) { + deleteButton.setOnTouchListener((View view, MotionEvent event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mMSDLPlayer != null) { + mMSDLPlayer.playToken(MSDLToken.KEYPRESS_DELETE, null); + } + return false; + }); + } else { + deleteButton.setOnTouchListener(mActionButtonTouchListener); + } deleteButton.setOnClickListener(v -> { // check for time-based lockouts if (mPasswordEntry.isEnabled()) { @@ -119,13 +137,19 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB if (mPasswordEntry.isEnabled()) { mView.resetPasswordText(true /* animate */, true /* announce */); } - mView.doHapticKeyClick(); + if (msdlFeedback() && mMSDLPlayer != null) { + mMSDLPlayer.playToken(MSDLToken.LONG_PRESS, null); + } else { + mView.doHapticKeyClick(); + } return true; }); View okButton = mView.findViewById(R.id.key_enter); if (okButton != null) { - okButton.setOnTouchListener(mActionButtonTouchListener); + if (!msdlFeedback()) { + okButton.setOnTouchListener(mActionButtonTouchListener); + } okButton.setOnClickListener(v -> { if (mPasswordEntry.isEnabled()) { verifyPasswordAndUnlock(); @@ -177,6 +201,7 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB for (NumPadKey button : mView.getButtons()) { button.setOnTouchListener(null); + button.setMSDLPlayer(null); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index f4cda0204036..3b5bf1a422a3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -18,6 +18,7 @@ package com.android.keyguard; import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; +import android.annotation.Nullable; import android.view.View; import com.android.internal.logging.UiEvent; @@ -33,6 +34,8 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.domain.MSDLPlayer; + public class KeyguardPinViewController extends KeyguardPinBasedInputViewController<KeyguardPINView> { private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @@ -61,11 +64,13 @@ public class KeyguardPinViewController FalsingCollector falsingCollector, DevicePostureController postureController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, - KeyguardKeyboardInteractor keyguardKeyboardInteractor) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor, + @Nullable MSDLPlayer msdlPlayer, + UserActivityNotifier userActivityNotifier) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor); + keyguardKeyboardInteractor, msdlPlayer, userActivityNotifier); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; mLockPatternUtils = lockPatternUtils; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 3ef3418bfed4..47fe2b22b5f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -21,6 +21,7 @@ import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; @@ -48,6 +49,8 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.domain.MSDLPlayer; + public class KeyguardSimPinViewController extends KeyguardPinBasedInputViewController<KeyguardSimPinView> { public static final String TAG = "KeyguardSimPinView"; @@ -95,11 +98,13 @@ public class KeyguardSimPinViewController TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - KeyguardKeyboardInteractor keyguardKeyboardInteractor) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor, + @Nullable MSDLPlayer msdlPlayer, + UserActivityNotifier userActivityNotifier) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor); + keyguardKeyboardInteractor, msdlPlayer, userActivityNotifier); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index 46225c7ea58a..c688acb8d760 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -17,6 +17,7 @@ package com.android.keyguard; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -43,6 +44,8 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.domain.MSDLPlayer; + public class KeyguardSimPukViewController extends KeyguardPinBasedInputViewController<KeyguardSimPukView> { private static final boolean DEBUG = KeyguardConstants.DEBUG; @@ -92,11 +95,13 @@ public class KeyguardSimPukViewController TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - KeyguardKeyboardInteractor keyguardKeyboardInteractor) { + KeyguardKeyboardInteractor keyguardKeyboardInteractor, + @Nullable MSDLPlayer msdlPlayer, + UserActivityNotifier userActivityNotifier) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, - keyguardKeyboardInteractor); + keyguardKeyboardInteractor, msdlPlayer, userActivityNotifier); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java index dcfa775dcabf..4fb80de2d4ec 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java @@ -15,6 +15,7 @@ */ package com.android.keyguard; +import static com.android.systemui.Flags.msdlFeedback; import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.ColorId.NUM_PAD_KEY; import android.content.Context; @@ -38,6 +39,9 @@ import androidx.annotation.Nullable; import com.android.settingslib.Utils; import com.android.systemui.res.R; +import com.google.android.msdl.data.model.MSDLToken; +import com.google.android.msdl.domain.MSDLPlayer; + /** * Viewgroup for the bouncer numpad button, specifically for digits. */ @@ -57,6 +61,8 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener { @Nullable private NumPadAnimator mAnimator; private int mOrientation; + @Nullable + private MSDLPlayer mMSDLPlayer; private View.OnClickListener mListener = new View.OnClickListener() { @Override @@ -221,8 +227,12 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener { // Cause a VIRTUAL_KEY vibration public void doHapticKeyClick() { - performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, - HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + if (msdlFeedback() && mMSDLPlayer != null) { + mMSDLPlayer.playToken(MSDLToken.KEYPRESS_STANDARD, null); + } else { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); + } } @Override @@ -244,4 +254,8 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener { super.onInitializeAccessibilityNodeInfo(info); info.setTextEntryKey(true); } + + public void setMSDLPlayer(@Nullable MSDLPlayer player) { + mMSDLPlayer = player; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java index 85f8b4824c99..0c4bc0ee3893 100644 --- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java +++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java @@ -16,6 +16,8 @@ package com.android.keyguard; +import static com.android.systemui.Flags.notifyPasswordTextViewUserActivityInBackground; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -257,7 +259,9 @@ public class PasswordTextView extends BasePasswordTextView { @Override protected void onUserActivity() { - mPM.userActivity(SystemClock.uptimeMillis(), false); + if (!notifyPasswordTextViewUserActivityInBackground()) { + mPM.userActivity(SystemClock.uptimeMillis(), false); + } super.onUserActivity(); } diff --git a/packages/SystemUI/src/com/android/keyguard/UserActivityNotifier.kt b/packages/SystemUI/src/com/android/keyguard/UserActivityNotifier.kt new file mode 100644 index 000000000000..9b1ddb74c3b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/UserActivityNotifier.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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.keyguard + +import android.os.PowerManager +import android.os.SystemClock +import com.android.systemui.dagger.qualifiers.UiBackground +import java.util.concurrent.Executor +import javax.inject.Inject + +/** Wrapper class for notifying the system about user activity in the background. */ +class UserActivityNotifier +@Inject +constructor( + @UiBackground private val uiBgExecutor: Executor, + private val powerManager: PowerManager +) { + + fun notifyUserActivity() { + uiBgExecutor.execute { + powerManager.userActivity( + SystemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_OTHER, + 0 + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java index 394f8dd629ae..04afd8693e04 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java @@ -408,6 +408,10 @@ public class FullscreenMagnificationController implements ComponentCallbacks { if (!isActivated()) { return; } + if (!(mFullscreenBorder.getBackground() instanceof GradientDrawable)) { + // Wear doesn't use the same magnification border background. So early return here. + return; + } float cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); GradientDrawable backgroundDrawable = (GradientDrawable) mFullscreenBorder.getBackground(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java index d718ae35dff0..708f7f1f34e4 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java @@ -30,8 +30,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Flags; -import com.android.wm.shell.common.bubbles.DismissCircleView; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.DismissCircleView; +import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import java.util.Map; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt index c1b3962ce0cb..13c1a450832f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt @@ -39,9 +39,9 @@ import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW import com.android.wm.shell.R -import com.android.wm.shell.common.bubbles.DismissCircleView -import com.android.wm.shell.common.bubbles.DismissView import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.shared.bubbles.DismissCircleView +import com.android.wm.shell.shared.bubbles.DismissView /** * View that handles interactions between DismissCircleView and BubbleStackView. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index d62162b368fa..7a674e2fa6f1 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -81,7 +81,7 @@ import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; import com.android.wm.shell.bubbles.DismissViewUtils; -import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; import java.lang.annotation.Retention; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt index 03f282e8f9a3..bb80396c70fb 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt @@ -120,42 +120,42 @@ interface QSAccessibilityModule { @IntoMap @StringKey(COLOR_CORRECTION_TILE_SPEC) fun provideColorCorrectionAvailabilityInteractor( - impl: ColorCorrectionTileDataInteractor + impl: ColorCorrectionTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(COLOR_INVERSION_TILE_SPEC) fun provideColorInversionAvailabilityInteractor( - impl: ColorCorrectionTileDataInteractor + impl: ColorCorrectionTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(FONT_SCALING_TILE_SPEC) fun provideFontScalingAvailabilityInteractor( - impl: FontScalingTileDataInteractor + impl: FontScalingTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(REDUCE_BRIGHTNESS_TILE_SPEC) fun provideReduceBrightnessAvailabilityInteractor( - impl: ReduceBrightColorsTileDataInteractor + impl: ReduceBrightColorsTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(ONE_HANDED_TILE_SPEC) fun provideOneHandedAvailabilityInteractor( - impl: OneHandedModeTileDataInteractor + impl: OneHandedModeTileDataInteractor ): QSTileAvailabilityInteractor @Binds @IntoMap @StringKey(NIGHT_DISPLAY_TILE_SPEC) fun provideNightDisplayAvailabilityInteractor( - impl: NightDisplayTileDataInteractor + impl: NightDisplayTileDataInteractor ): QSTileAvailabilityInteractor companion object { @@ -165,6 +165,7 @@ interface QSAccessibilityModule { const val REDUCE_BRIGHTNESS_TILE_SPEC = "reduce_brightness" const val ONE_HANDED_TILE_SPEC = "onehanded" const val NIGHT_DISPLAY_TILE_SPEC = "night" + const val HEARING_DEVICES_TILE_SPEC = "hearing_devices" @Provides @IntoMap @@ -273,6 +274,20 @@ interface QSAccessibilityModule { instanceId = uiEventLogger.getNewInstanceId(), ) + @Provides + @IntoMap + @StringKey(HEARING_DEVICES_TILE_SPEC) + fun provideHearingDevicesTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(HEARING_DEVICES_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_hearing_devices_icon, + labelRes = R.string.quick_settings_hearing_devices_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + ) + /** * Inject Reduce Bright Colors Tile into tileViewModelMap in QSModule. The tile is hidden * behind a flag. diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index 468737d9372f..732a90d2c01d 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -32,11 +32,11 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim import com.android.systemui.authentication.shared.model.AuthenticationResultModel +import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.onSubscriberAdded @@ -254,7 +254,7 @@ constructor( override val hasLockoutOccurred: StateFlow<Boolean> = _hasLockoutOccurred.asStateFlow() init { - if (SceneContainerFlag.isEnabled) { + if (ComposeBouncerFlags.isComposeBouncerOrSceneContainerEnabled()) { // Hydrate failedAuthenticationAttempts initially and whenever the selected user // changes. applicationScope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 25d43d972fe2..4c2fe07f92bb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -941,16 +941,16 @@ constructor( private fun vibrateOnSuccess() { _hapticsToPlay.value = HapticsToPlay( - HapticFeedbackConstants.CONFIRM, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + HapticFeedbackConstants.BIOMETRIC_CONFIRM, + null, ) } private fun vibrateOnError() { _hapticsToPlay.value = HapticsToPlay( - HapticFeedbackConstants.REJECT, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + HapticFeedbackConstants.BIOMETRIC_REJECT, + null, ) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt index 62ef365345b7..a1111f68f1ee 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlags.kt @@ -17,18 +17,17 @@ package com.android.systemui.bouncer.shared.flag import com.android.systemui.Flags -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.flag.SceneContainerFlag -import dagger.Module -import dagger.Provides -interface ComposeBouncerFlags { +object ComposeBouncerFlags { /** * Returns `true` if the Compose bouncer is enabled or if the scene container framework is * enabled; `false` otherwise. */ - fun isComposeBouncerOrSceneContainerEnabled(): Boolean + fun isComposeBouncerOrSceneContainerEnabled(): Boolean { + return SceneContainerFlag.isEnabled || Flags.composeBouncer() + } /** * Returns `true` if only compose bouncer is enabled and scene container framework is not @@ -39,30 +38,7 @@ interface ComposeBouncerFlags { "that includes compose bouncer in legacy keyguard.", replaceWith = ReplaceWith("isComposeBouncerOrSceneContainerEnabled()") ) - fun isOnlyComposeBouncerEnabled(): Boolean -} - -class ComposeBouncerFlagsImpl() : ComposeBouncerFlags { - - override fun isComposeBouncerOrSceneContainerEnabled(): Boolean { - return SceneContainerFlag.isEnabled || Flags.composeBouncer() - } - - @Deprecated( - "Avoid using this, this is meant to be used only by the glue code " + - "that includes compose bouncer in legacy keyguard.", - replaceWith = ReplaceWith("isComposeBouncerOrSceneContainerEnabled()") - ) - override fun isOnlyComposeBouncerEnabled(): Boolean { + fun isOnlyComposeBouncerEnabled(): Boolean { return !SceneContainerFlag.isEnabled && Flags.composeBouncer() } } - -@Module -object ComposeBouncerFlagsModule { - @Provides - @SysUISingleton - fun impl(): ComposeBouncerFlags { - return ComposeBouncerFlagsImpl() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt index ad93a25f39a5..cc8dce7938aa 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt @@ -55,12 +55,11 @@ constructor( class BouncerViewBinder @Inject constructor( - private val composeBouncerFlags: ComposeBouncerFlags, private val legacyBouncerDependencies: Lazy<LegacyBouncerDependencies>, private val composeBouncerDependencies: Lazy<ComposeBouncerDependencies>, ) { fun bind(view: ViewGroup) { - if (composeBouncerFlags.isOnlyComposeBouncerEnabled()) { + if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) { val deps = composeBouncerDependencies.get() ComposeBouncerViewBinder.bind( view, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt index 102ae7abb3e2..c4bbd9cf0d9f 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt @@ -8,14 +8,12 @@ import androidx.compose.ui.platform.ComposeView import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.compose.theme.PlatformTheme import com.android.keyguard.ViewMediatorCallback import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerDialogFactory -import com.android.systemui.bouncer.ui.composable.BouncerContent +import com.android.systemui.bouncer.ui.composable.BouncerContainer import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel -import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.user.domain.interactor.SelectedUserInteractor import kotlinx.coroutines.flow.collectLatest @@ -49,16 +47,7 @@ object ComposeBouncerViewBinder { this@repeatWhenAttached.lifecycle } ) - setContent { - PlatformTheme { - BouncerContent( - rememberViewModel("ComposeBouncerViewBinder") { - viewModelFactory.create() - }, - dialogFactory, - ) - } - } + setContent { BouncerContainer(viewModelFactory, dialogFactory) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/composable/BouncerContainer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/composable/BouncerContainer.kt new file mode 100644 index 000000000000..c05dcd5cea83 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/composable/BouncerContainer.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 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.bouncer.ui.composable + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.android.compose.theme.PlatformTheme +import com.android.systemui.bouncer.ui.BouncerDialogFactory +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel +import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.lifecycle.rememberViewModel + +/** Container that includes the compose bouncer and is meant to be included in legacy keyguard. */ +@Composable +fun BouncerContainer( + viewModelFactory: BouncerSceneContentViewModel.Factory, + dialogFactory: BouncerDialogFactory, +) { + PlatformTheme { + val backgroundColor = MaterialTheme.colorScheme.surface + + val bouncerViewModel = rememberViewModel("BouncerContainer") { viewModelFactory.create() } + Box { + Canvas(Modifier.fillMaxSize()) { drawRect(color = backgroundColor) } + + // Separate the bouncer content into a reusable composable that + // doesn't have any SceneScope + // dependencies + BouncerContent( + bouncerViewModel, + dialogFactory, + Modifier.sysuiResTag(Bouncer.TestTags.Root).fillMaxSize() + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt index e54dc7dbdebb..c383b8d5f95c 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt @@ -78,7 +78,6 @@ constructor( private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor, - private val flags: ComposeBouncerFlags, ) : ExclusiveActivatable() { /** * A message shown when the user has attempted the wrong credential too many times and now must @@ -96,7 +95,7 @@ constructor( val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) override suspend fun onActivated(): Nothing { - if (!flags.isComposeBouncerOrSceneContainerEnabled()) { + if (!ComposeBouncerFlags.isComposeBouncerOrSceneContainerEnabled()) { return awaitCancellation() } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt index adc4bc9a14f3..0aada06a7eb7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt @@ -29,7 +29,6 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text @@ -57,7 +56,6 @@ constructor( private val authenticationInteractor: AuthenticationInteractor, private val devicePolicyManager: DevicePolicyManager, private val bouncerMessageViewModelFactory: BouncerMessageViewModel.Factory, - private val flags: ComposeBouncerFlags, private val userSwitcher: UserSwitcherViewModel, private val actionButtonInteractor: BouncerActionButtonInteractor, private val pinViewModelFactory: PinBouncerViewModel.Factory, diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 9b96341bdd8e..b570e14c646a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -61,6 +61,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.phone.ManagedProfileController import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.emitOnStart @@ -116,6 +117,7 @@ constructor( sceneInteractor: SceneInteractor, @CommunalLog logBuffer: LogBuffer, @CommunalTableLog tableLogBuffer: TableLogBuffer, + private val managedProfileController: ManagedProfileController ) { private val logger = Logger(logBuffer, "CommunalInteractor") @@ -401,12 +403,7 @@ constructor( /** Request to unpause work profile that is currently in quiet mode. */ fun unpauseWorkProfile() { - userTracker.userProfiles - .find { it.isManagedProfile } - ?.userHandle - ?.let { userHandle -> - userManager.requestQuietModeEnabled(/* enableQuietMode */ false, userHandle) - } + managedProfileController.setWorkModeEnabled(true) } /** Returns true if work profile is in quiet mode (disabled) for user handle. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt index 8f756a23a9da..ac496f01a39d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt @@ -195,7 +195,7 @@ constructor( is ObservableTransitionState.Idle -> flowOf(CommunalTransitionProgressModel.Idle(state.currentScene)) is ObservableTransitionState.Transition -> - if (state.toScene == targetScene) { + if (state.toContent == targetScene) { state.progress.map { CommunalTransitionProgressModel.Transition( // Clamp the progress values between 0 and 1 as actual progress diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt index e04d3095d68d..c7538bb4f696 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt @@ -161,7 +161,7 @@ constructor( if ( prevTransition is ObservableTransitionState.Transition && currentTransitionId != null && - idle.currentScene == prevTransition.toScene + idle.currentScene == prevTransition.toContent ) { finishCurrentTransition() } else { @@ -219,17 +219,19 @@ constructor( prevTransition: ObservableTransitionState, transition: ObservableTransitionState.Transition ) { - if (prevTransition.isTransitioning(from = transition.fromScene, to = transition.toScene)) { + if ( + prevTransition.isTransitioning(from = transition.fromContent, to = transition.toContent) + ) { // This is a new transition, but exactly the same as the previous state. Skip resetting // KTF for this case and just collect the new progress instead. collectProgress(transition) - } else if (transition.toScene == CommunalScenes.Communal) { + } else if (transition.toContent == CommunalScenes.Communal) { if (currentToState == KeyguardState.GLANCEABLE_HUB) { transitionKtfTo(transitionInteractor.startedKeyguardTransitionStep.value.from) } startTransitionToGlanceableHub() collectProgress(transition) - } else if (transition.toScene == CommunalScenes.Blank) { + } else if (transition.toContent == CommunalScenes.Blank) { // Another transition started before this one is completed. Transition to the // GLANCEABLE_HUB state so that we can properly transition away from it. transitionKtfTo(KeyguardState.GLANCEABLE_HUB) diff --git a/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt index 2352841fdde9..1def5a3147bc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/log/CommunalLoggerStartable.kt @@ -126,13 +126,13 @@ private fun ObservableTransitionState.isNotOnCommunal(): Boolean { /** Whether currently transitioning from another scene to communal. */ private fun ObservableTransitionState.isSwipingToCommunal(): Boolean { return this is ObservableTransitionState.Transition && - toScene == CommunalScenes.Communal && + toContent == CommunalScenes.Communal && isInitiatedByUserInput } /** Whether currently transitioning from communal to another scene. */ private fun ObservableTransitionState.isSwipingFromCommunal(): Boolean { return this is ObservableTransitionState.Transition && - fromScene == CommunalScenes.Communal && + fromContent == CommunalScenes.Communal && isInitiatedByUserInput } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalSceneLogger.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalSceneLogger.kt index aed92156cfc3..83f31e54e92e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalSceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/log/CommunalSceneLogger.kt @@ -74,8 +74,8 @@ class CommunalSceneLogger @Inject constructor(@CommunalLog private val logBuffer tag = TAG, level = LogLevel.INFO, messageInitializer = { - str1 = transitionState.fromScene.toString() - str2 = transitionState.toScene.toString() + str1 = transitionState.fromContent.toString() + str2 = transitionState.toContent.toString() }, messagePrinter = { "Scene transition started: $str1 → $str2" }, ) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index d288ccee2ae8..37c6e17de148 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -391,8 +391,10 @@ constructor( ), Pair( if (SceneContainerFlag.isEnabled) { - keyguardTransitionInteractor - .isInTransitionWhere(toStatePredicate = { it == KeyguardState.UNDEFINED }) + sceneInteractor + .get() + .transitionState + .map { it.isTransitioning(to = Scenes.Gone) || it.isIdle(Scenes.Gone) } .isFalse() } else { keyguardRepository.isKeyguardGoingAway.isFalse() diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt index f5914104d87f..cdd2b054711e 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractor.kt @@ -65,7 +65,7 @@ interface DeviceEntryFaceAuthInteractor : CoreStartable { fun onDeviceLifted() - fun onQsExpansionStarted() + fun onShadeExpansionStarted() fun onNotificationPanelClicked() diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 39f4e31fa3cd..7018f9dc8556 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -16,12 +16,14 @@ package com.android.systemui.deviceentry.domain.interactor +import com.android.internal.policy.IKeyguardDismissCallback import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository +import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.utils.coroutines.flow.mapLatestConflated @@ -56,6 +58,7 @@ constructor( private val sceneInteractor: SceneInteractor, private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val alternateBouncerInteractor: AlternateBouncerInteractor, + private val dismissCallbackRegistry: DismissCallbackRegistry, ) { /** * Whether the device is unlocked. @@ -126,17 +129,14 @@ constructor( }, isLockscreenEnabled, deviceUnlockedInteractor.deviceUnlockStatus, - isDeviceEntered) { - isNoneAuthMethod, - isLockscreenEnabled, - deviceUnlockStatus, - isDeviceEntered -> - val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled - (isSwipeAuthMethod || - (deviceUnlockStatus.isUnlocked && - deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) && - !isDeviceEntered - } + isDeviceEntered + ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered -> + val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled + (isSwipeAuthMethod || + (deviceUnlockStatus.isUnlocked && + deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) && + !isDeviceEntered + } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, @@ -150,8 +150,16 @@ constructor( /** * Attempt to enter the device and dismiss the lockscreen. If authentication is required to * unlock the device it will transition to bouncer. + * + * @param callback An optional callback to invoke when the attempt succeeds, fails, or is + * canceled */ - fun attemptDeviceEntry() { + @JvmOverloads + fun attemptDeviceEntry( + callback: IKeyguardDismissCallback? = null, + ) { + callback?.let { dismissCallbackRegistry.addCallback(it) } + // TODO (b/307768356), // 1. Check if the device is already authenticated by trust agent/passive biometrics // 2. Show SPFS/UDFPS bouncer if it is available AlternateBouncerInteractor.show diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt index b7d2a57d9a41..9b8c2b1acc33 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/NoopDeviceEntryFaceAuthInteractor.kt @@ -60,7 +60,7 @@ class NoopDeviceEntryFaceAuthInteractor @Inject constructor() : DeviceEntryFaceA override fun onDeviceLifted() {} - override fun onQsExpansionStarted() {} + override fun onShadeExpansionStarted() {} override fun onNotificationPanelClicked() {} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index 5ef63d9b856c..3b5d5a8f0598 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -220,9 +220,9 @@ constructor( sceneInteractor .get() .transitionState - .filter { it.isTransitioning(from = Scenes.QuickSettings, to = Scenes.Shade) } + .filter { it.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) } .distinctUntilChanged() - .onEach { onQsExpansionStarted() } + .onEach { onShadeExpansionStarted() } .launchIn(applicationScope) } } @@ -250,8 +250,8 @@ constructor( runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, true) } - override fun onQsExpansionStarted() { - runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true) + override fun onShadeExpansionStarted() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, false) } override fun onDeviceLifted() { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index e3f740e6ff72..1c263ae4b2bb 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -185,6 +185,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mShadeExpanded = expanded; updateLifecycleStateLocked(); + updateGestureBlockingLocked(); }); } }; @@ -215,6 +216,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mBouncerShowing = bouncerShowing; updateLifecycleStateLocked(); + updateGestureBlockingLocked(); }); } }; @@ -248,7 +250,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ * others. */ public void reset(String source) { - reset(()-> {}, source); + reset(() -> {}, source); } /** @@ -525,11 +527,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStarted = true; updateRedirectWakeup(); - - if (!isDreamInPreviewMode()) { - mGestureInteractor.addGestureBlockedMatcher(DREAM_TYPE_MATCHER, - GestureInteractor.Scope.Global); - } + updateGestureBlockingLocked(); } private void updateRedirectWakeup() { @@ -553,6 +551,18 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ null); } + private void updateGestureBlockingLocked() { + final boolean shouldBlock = !isDreamInPreviewMode() && !mShadeExpanded && !mBouncerShowing; + + if (shouldBlock) { + mGestureInteractor.addGestureBlockedMatcher(DREAM_TYPE_MATCHER, + GestureInteractor.Scope.Global); + } else { + mGestureInteractor.removeGestureBlockedMatcher(DREAM_TYPE_MATCHER, + GestureInteractor.Scope.Global); + } + } + private Lifecycle.State getLifecycleStateLocked() { return mLifecycleRegistry.getCurrentState(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 0feb5ec277b4..1bc91cac1a84 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -76,6 +76,7 @@ import com.android.keyguard.mediator.ScreenOnCoordinator; import com.android.systemui.SystemUIApplication; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.domain.interactor.KeyguardDismissInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor; @@ -122,6 +123,7 @@ public class KeyguardService extends Service { private final PowerInteractor mPowerInteractor; private final KeyguardInteractor mKeyguardInteractor; private final Lazy<SceneInteractor> mSceneInteractorLazy; + private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy; private final Executor mMainExecutor; private final Lazy<KeyguardStateCallbackStartable> mKeyguardStateCallbackStartableLazy; @@ -347,7 +349,8 @@ public class KeyguardService extends Service { KeyguardEnabledInteractor keyguardEnabledInteractor, Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy, KeyguardWakeDirectlyToGoneInteractor keyguardWakeDirectlyToGoneInteractor, - KeyguardDismissInteractor keyguardDismissInteractor) { + KeyguardDismissInteractor keyguardDismissInteractor, + Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy) { super(); mKeyguardViewMediator = keyguardViewMediator; mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher; @@ -360,6 +363,7 @@ public class KeyguardService extends Service { mSceneInteractorLazy = sceneInteractorLazy; mMainExecutor = mainExecutor; mKeyguardStateCallbackStartableLazy = keyguardStateCallbackStartableLazy; + mDeviceEntryInteractorLazy = deviceEntryInteractorLazy; if (KeyguardWmStateRefactor.isEnabled()) { WindowManagerLockscreenVisibilityViewBinder.bind( @@ -484,7 +488,9 @@ public class KeyguardService extends Service { public void dismiss(IKeyguardDismissCallback callback, CharSequence message) { trace("dismiss message=" + message); checkPermission(); - if (KeyguardWmStateRefactor.isEnabled()) { + if (SceneContainerFlag.isEnabled()) { + mDeviceEntryInteractorLazy.get().attemptDeviceEntry(callback); + } else if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardDismissInteractor.dismissKeyguardWithCallback(callback); } else { mKeyguardViewMediator.dismiss(callback, message); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 8c82900810be..d38c9520eb7a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -3568,12 +3568,16 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } return; } - try { - mStatusBarService.disableForUser(flags, mStatusBarDisableToken, - mContext.getPackageName(), - mSelectedUserInteractor.getSelectedUserId()); - } catch (RemoteException e) { - Log.d(TAG, "Failed to set disable flags: " + flags, e); + + // Handled in StatusBarDisableFlagsInteractor. + if (!KeyguardWmStateRefactor.isEnabled()) { + try { + mStatusBarService.disableForUser(flags, mStatusBarDisableToken, + mContext.getPackageName(), + mSelectedUserInteractor.getSelectedUserId()); + } catch (RemoteException e) { + Log.d(TAG, "Failed to set disable flags: " + flags, e); + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 2af95f20c7d7..2c3b481b9e16 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -119,7 +119,8 @@ constructor( is ObservableTransitionState.Idle -> it.currentScene == Scenes.Lockscreen is ObservableTransitionState.Transition -> - it.fromScene == Scenes.Lockscreen || it.toScene == Scenes.Lockscreen + it.fromContent == Scenes.Lockscreen || + it.toContent == Scenes.Lockscreen } } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index d11a41eac3a1..e19b72e26567 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.util.kotlin.WithPrev import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -88,6 +89,18 @@ constructor( val transitionState: StateFlow<TransitionStep> = transitions.stateIn(scope, SharingStarted.Eagerly, TransitionStep()) + private val sceneTransitionPair = + sceneInteractor.transitionState + .pairwise() + .stateIn( + scope, + SharingStarted.Eagerly, + WithPrev( + sceneInteractor.transitionState.value, + sceneInteractor.transitionState.value + ) + ) + /** * A pair of the most recent STARTED step, and the transition step immediately preceding it. The * transition framework enforces that the previous step is either a CANCELED or FINISHED step, @@ -194,7 +207,7 @@ constructor( } return if (SceneContainerFlag.isEnabled) { - flow.filter { + flow.filter { step -> val fromScene = when (edge) { is Edge.StateToState -> edge.from?.mapToSceneContainerScene() @@ -211,8 +224,23 @@ constructor( fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null - return@filter (fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull()) || + val isTransitioningBetweenLockscreenStates = + fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull() + val isTransitioningBetweenDesiredScenes = sceneInteractor.transitionState.value.isTransitioning(fromScene, toScene) + + // We can't compare the terminal step with the current sceneTransition because + // a) STL has no guarantee that it will settle in Idle() when finished/canceled + // b) Comparing to Idle(toScene) would make any other FINISHED step settling in + // toScene pass as well + val terminalStepBelongsToPreviousTransition = + (step.transitionState == TransitionState.FINISHED || + step.transitionState == TransitionState.CANCELED) && + sceneTransitionPair.value.previousValue.isTransitioning(fromScene, toScene) + + return@filter isTransitioningBetweenLockscreenStates || + isTransitioningBetweenDesiredScenes || + terminalStepBelongsToPreviousTransition } } else { flow diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index ac874005b612..a09cd7c12d42 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -127,8 +127,8 @@ constructor( when (transitionState) { is ObservableTransitionState.Transition -> when { - transitionState.fromScene == Scenes.Lockscreen && - transitionState.toScene == Scenes.Gone -> + transitionState.fromContent == Scenes.Lockscreen && + transitionState.toContent == Scenes.Gone -> sceneInteractor .get() .isTransitionUserInputOngoing @@ -139,8 +139,8 @@ constructor( flowOf(true) } } - transitionState.fromScene == Scenes.Bouncer && - transitionState.toScene == Scenes.Gone -> + transitionState.fromContent == Scenes.Bouncer && + transitionState.toContent == Scenes.Gone -> transitionState.progress.map { progress -> progress > FromPrimaryBouncerTransitionInteractor diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt index ffd7812166db..f3bb8293851f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -111,7 +111,7 @@ constructor( if (currentTransitionId == null) return if (prevTransition !is ObservableTransitionState.Transition) return - if (idle.currentScene == prevTransition.toScene) { + if (idle.currentScene == prevTransition.toContent) { finishCurrentTransition() } else { val targetState = @@ -150,7 +150,7 @@ constructor( } private suspend fun handleTransition(transition: ObservableTransitionState.Transition) { - if (transition.fromScene == Scenes.Lockscreen) { + if (transition.fromContent == Scenes.Lockscreen) { if (currentTransitionId != null) { val currentToState = internalTransitionInteractor.currentTransitionInfoInternal.value.to @@ -160,7 +160,7 @@ constructor( } startTransitionFromLockscreen() collectProgress(transition) - } else if (transition.toScene == Scenes.Lockscreen) { + } else if (transition.toContent == Scenes.Lockscreen) { if (currentTransitionId != null) { transitionKtfTo(UNDEFINED) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index bec8f3da9999..f1b9cba11051 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -93,7 +93,7 @@ object KeyguardBlueprintViewBinder { blueprint.applyConstraints(this) } - logAlphaVisibilityOfAppliedConstraintSet(cs, clockViewModel) + logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel) cs.applyTo(constraintLayout) } } @@ -115,7 +115,7 @@ object KeyguardBlueprintViewBinder { clone(constraintLayout) blueprint.applyConstraints(this) } - logAlphaVisibilityOfAppliedConstraintSet(cs, clockViewModel) + logAlphaVisibilityScaleOfAppliedConstraintSet(cs, clockViewModel) cs.applyTo(constraintLayout) } } @@ -124,7 +124,7 @@ object KeyguardBlueprintViewBinder { } } - private fun logAlphaVisibilityOfAppliedConstraintSet( + private fun logAlphaVisibilityScaleOfAppliedConstraintSet( cs: ConstraintSet, viewModel: KeyguardClockViewModel ) { @@ -136,12 +136,15 @@ object KeyguardBlueprintViewBinder { Log.i( TAG, "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " + - "alpha=${cs.getConstraint(smallClockViewId).propertySet.alpha}" + "alpha=${cs.getConstraint(smallClockViewId).propertySet.alpha} " + + "scale=${cs.getConstraint(smallClockViewId).transform.scaleX} " ) Log.i( TAG, "applyCsToLargeClock: vis=${cs.getVisibility(largeClockViewId)} " + - "alpha=${cs.getConstraint(largeClockViewId).propertySet.alpha}" + "alpha=${cs.getConstraint(largeClockViewId).propertySet.alpha} " + + "scale=${cs.getConstraint(largeClockViewId).transform.scaleX} " + + "pivotX=${cs.getConstraint(largeClockViewId).transform.transformPivotX} " ) Log.i( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index a7a832148130..7899971484f1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -369,8 +369,7 @@ object KeyguardRootViewBinder { } else { vibratorHelper.performHapticFeedback( view, - HapticFeedbackConstants.CONFIRM, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + HapticFeedbackConstants.BIOMETRIC_CONFIRM, ) } } @@ -390,8 +389,7 @@ object KeyguardRootViewBinder { } else { vibratorHelper.performHapticFeedback( view, - HapticFeedbackConstants.REJECT, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + HapticFeedbackConstants.BIOMETRIC_REJECT, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index c8fe55d2a7c4..be6b0eb79afe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -59,6 +59,16 @@ internal fun ConstraintSet.setAlpha( alpha: Float, ) = views.forEach { view -> this.setAlpha(view.id, alpha) } +internal fun ConstraintSet.setScaleX( + views: Iterable<View>, + alpha: Float, +) = views.forEach { view -> this.setScaleX(view.id, alpha) } + +internal fun ConstraintSet.setScaleY( + views: Iterable<View>, + alpha: Float, +) = views.forEach { view -> this.setScaleY(view.id, alpha) } + @SysUISingleton class ClockSection @Inject @@ -125,6 +135,9 @@ constructor( setAlpha(getNonTargetClockFace(clock).views, 0F) if (!keyguardClockViewModel.isLargeClockVisible.value) { connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM) + } else { + setScaleX(getTargetClockFace(clock).views, rootViewModel.burnInModel.value.scale) + setScaleY(getTargetClockFace(clock).views, rootViewModel.burnInModel.value.scale) } } } @@ -205,6 +218,9 @@ constructor( create(R.id.small_clock_guideline_top, ConstraintSet.HORIZONTAL_GUIDELINE) setGuidelineBegin(R.id.small_clock_guideline_top, smallClockTopMargin) connect(R.id.lockscreen_clock_view, TOP, R.id.small_clock_guideline_top, BOTTOM) + + // Explicitly clear pivot to force recalculate pivot instead of using legacy value + setTransformPivot(R.id.lockscreen_clock_view_large, Float.NaN, Float.NaN) } constrainWeatherClockDateIconsBarrier(constraints) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index 55fc71823685..99160f8a9158 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -127,6 +127,7 @@ constructor( // migrate addSmartspaceView from KeyguardClockSwitchController constrainHeight(sharedR.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(sharedR.id.bc_smartspace_view, ConstraintSet.MATCH_CONSTRAINT) connect( sharedR.id.bc_smartspace_view, ConstraintSet.START, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index ebdcaa0c91a6..eaa61a113ee6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -34,20 +34,18 @@ import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING -import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN -import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.ui.StateToValue import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.util.kotlin.BooleanFlowOperators.any import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample @@ -86,6 +84,7 @@ constructor( private val communalInteractor: CommunalInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, + notificationShadeWindowModel: NotificationShadeWindowModel, private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel, private val alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel, @@ -197,37 +196,18 @@ constructor( .distinctUntilChanged() /** - * Keyguard states which should fully hide the keyguard. - * - * Note: [GONE] is not included as it is handled separately. - */ - private val hiddenKeyguardStates = listOf(OCCLUDED, DREAMING, GLANCEABLE_HUB) - - /** * Keyguard should not show if fully transitioned into a hidden keyguard state or if * transitioning between hidden states. */ private val hideKeyguard: Flow<Boolean> = - (hiddenKeyguardStates.map { state -> - keyguardTransitionInteractor - .transitionValue(state) - .map { it == 1f } - .onStart { emit(false) } - } + - listOf( - communalInteractor.isIdleOnCommunal, - keyguardTransitionInteractor - .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE) - .map { it == 1f } - .onStart { emit(false) }, - keyguardTransitionInteractor - .isInTransitionWhere( - fromStatePredicate = { hiddenKeyguardStates.contains(it) }, - toStatePredicate = { hiddenKeyguardStates.contains(it) }, - ) - .onStart { emit(false) }, - )) - .any() + anyOf( + notificationShadeWindowModel.isKeyguardOccluded, + communalInteractor.isIdleOnCommunal, + keyguardTransitionInteractor + .transitionValue(scene = Scenes.Gone, stateWithoutSceneContainer = GONE) + .map { it == 1f } + .onStart { emit(false) }, + ) /** Last point that the root view was tapped */ val lastRootViewTapPosition: Flow<Point?> = keyguardInteractor.lastRootViewTapPosition diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index 378a147c2c82..bcf748e7573f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -59,13 +59,14 @@ fun createActionsFromState( val playOrPause = if (isConnectingState(state.state)) { // Spinner needs to be animating to render anything. Start it here. - val drawable = MediaControlDrawables.getProgress(context) + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) (drawable as Animatable).start() MediaAction( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - MediaControlDrawables.getConnecting(context), + context.getDrawable(R.drawable.ic_media_connecting_container), // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material ) @@ -153,18 +154,18 @@ private fun getStandardAction( return when (action) { PlaybackState.ACTION_PLAY -> { MediaAction( - MediaControlDrawables.getPlayIcon(context), + context.getDrawable(R.drawable.ic_media_play), { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - MediaControlDrawables.getPlayBackground(context) + context.getDrawable(R.drawable.ic_media_play_container) ) } PlaybackState.ACTION_PAUSE -> { MediaAction( - MediaControlDrawables.getPauseIcon(context), + context.getDrawable(R.drawable.ic_media_pause), { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - MediaControlDrawables.getPauseBackground(context) + context.getDrawable(R.drawable.ic_media_pause_container) ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index 415449f4454f..4555810ee0ef 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -71,7 +71,6 @@ import com.android.systemui.media.controls.data.repository.MediaDataRepository import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser -import com.android.systemui.media.controls.shared.MediaControlDrawables import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC import com.android.systemui.media.controls.shared.model.MediaAction @@ -1229,7 +1228,7 @@ class MediaDataProcessor( .loadDrawable(context), action, context.getString(R.string.controls_media_resume), - MediaControlDrawables.getPlayBackground(context) + context.getDrawable(R.drawable.ic_media_play_container) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt index c78220e42d1a..95ca11c9b451 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt @@ -17,20 +17,12 @@ package com.android.systemui.media.controls.shared import android.content.Context -import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable import com.android.systemui.Flags.mediaControlsDrawablesReuse import com.android.systemui.res.R object MediaControlDrawables { - // Play/Pause Button drawables. - private var progress: Drawable? = null - private var connecting: Drawable? = null - private var playIcon: AnimatedVectorDrawable? = null - private var playBackground: AnimatedVectorDrawable? = null - private var pauseIcon: AnimatedVectorDrawable? = null - private var pauseBackground: AnimatedVectorDrawable? = null // Prev button. private var prevIcon: Drawable? = null // Next button. @@ -40,81 +32,6 @@ object MediaControlDrawables { private var antenna: Drawable? = null private var groupDevice: Drawable? = null private var homeDevices: Drawable? = null - // Guts drawables. - private var outline: Drawable? = null - private var solid: Drawable? = null - - fun getProgress(context: Context): Drawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(com.android.internal.R.drawable.progress_small_material) - } - return progress?.mutate() - ?: context.getDrawable(com.android.internal.R.drawable.progress_small_material).also { - progress = it - } - } - - fun getConnecting(context: Context): Drawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(R.drawable.ic_media_connecting_container) - } - return connecting?.mutate() - ?: context.getDrawable(R.drawable.ic_media_connecting_container).also { - connecting = it - } - } - - fun getPlayIcon(context: Context): AnimatedVectorDrawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(R.drawable.ic_media_play) as AnimatedVectorDrawable? - } - return playIcon?.let { - it.reset() - it.mutate() as AnimatedVectorDrawable - } - ?: (context.getDrawable(R.drawable.ic_media_play) as AnimatedVectorDrawable?).also { - playIcon = it - } - } - - fun getPlayBackground(context: Context): AnimatedVectorDrawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(R.drawable.ic_media_play_container) - as AnimatedVectorDrawable? - } - return playBackground?.let { - it.reset() - it.mutate() as AnimatedVectorDrawable - } - ?: (context.getDrawable(R.drawable.ic_media_play_container) as AnimatedVectorDrawable?) - .also { playBackground = it } - } - - fun getPauseIcon(context: Context): AnimatedVectorDrawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(R.drawable.ic_media_pause) as AnimatedVectorDrawable? - } - return pauseIcon?.let { - it.reset() - it.mutate() as AnimatedVectorDrawable - } - ?: (context.getDrawable(R.drawable.ic_media_pause) as AnimatedVectorDrawable?).also { - pauseIcon = it - } - } - - fun getPauseBackground(context: Context): AnimatedVectorDrawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(R.drawable.ic_media_pause_container) - as AnimatedVectorDrawable? - } - return pauseBackground?.let { - it.reset() - it.mutate() as AnimatedVectorDrawable - } - ?: (context.getDrawable(R.drawable.ic_media_pause_container) as AnimatedVectorDrawable?) - .also { pauseBackground = it } - } fun getNextIcon(context: Context): Drawable? { if (!mediaControlsDrawablesReuse()) { @@ -165,19 +82,4 @@ object MediaControlDrawables { return homeDevices ?: context.getDrawable(R.drawable.ic_media_home_devices).also { homeDevices = it } } - - fun getOutline(context: Context): Drawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(R.drawable.qs_media_outline_button) - } - return outline - ?: context.getDrawable(R.drawable.qs_media_outline_button).also { outline = it } - } - - fun getSolid(context: Context): Drawable? { - if (!mediaControlsDrawablesReuse()) { - return context.getDrawable(R.drawable.qs_media_solid_button) - } - return solid ?: context.getDrawable(R.drawable.qs_media_solid_button).also { solid = it } - } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt index f4601340ee42..64820e0d0ced 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt @@ -30,7 +30,6 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor -import com.android.systemui.media.controls.shared.MediaControlDrawables import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaControlModel @@ -285,9 +284,9 @@ class MediaControlViewModel( }, cancelTextBackground = if (model.isDismissible) { - MediaControlDrawables.getOutline(applicationContext) + applicationContext.getDrawable(R.drawable.qs_media_outline_button) } else { - MediaControlDrawables.getSolid(applicationContext) + applicationContext.getDrawable(R.drawable.qs_media_solid_button) }, onSettingsClicked = { logger.logLongPressSettings(model.uid, model.packageName, model.instanceId) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index c706c3e97c96..e8c90c1fc9bf 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -140,7 +140,6 @@ import com.android.systemui.settings.UserContextProvider; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor; -import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.rotation.RotationButtonController; import com.android.systemui.shared.rotation.RotationPolicyUtil; @@ -166,6 +165,7 @@ import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.ViewController; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; import dagger.Lazy; diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt new file mode 100644 index 000000000000..6ef83e262ac8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayActionsViewModel.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 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.notifications.ui.viewmodel + +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Models the UI state for the user actions for navigating to other scenes or overlays. */ +class NotificationsShadeOverlayActionsViewModel @AssistedInject constructor() : + SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + setActions( + mapOf( + Swipe.Up to UserActionResult.HideOverlay(Overlays.NotificationsShade), + Back to UserActionResult.HideOverlay(Overlays.NotificationsShade), + ) + ) + } + + @AssistedFactory + interface Factory { + fun create(): NotificationsShadeOverlayActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt new file mode 100644 index 000000000000..5be225c718ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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.notifications.ui.viewmodel + +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Models UI state used to render the content of the notifications shade overlay. + * + * Different from [NotificationsShadeOverlayActionsViewModel], which only models user actions that + * can be performed to navigate to other scenes. + */ +class NotificationsShadeOverlayContentViewModel +@AssistedInject +constructor( + val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, + val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, + private val sceneInteractor: SceneInteractor, +) { + fun onScrimClicked() { + sceneInteractor.hideOverlay( + overlay = Overlays.NotificationsShade, + loggingReason = "Shade scrim clicked", + ) + } + + @AssistedFactory + interface Factory { + fun create(): NotificationsShadeOverlayContentViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt index 9fb09c03834c..572a0caf49f2 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt @@ -22,28 +22,19 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeAlignment import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject /** * Models the UI state for the user actions that the user can perform to navigate to other scenes. */ -class NotificationsShadeSceneActionsViewModel -@AssistedInject -constructor( - private val shadeInteractor: ShadeInteractor, -) : SceneActionsViewModel() { +class NotificationsShadeSceneActionsViewModel @AssistedInject constructor() : + SceneActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { setActions( mapOf( - if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) { - Swipe.Up - } else { - Swipe.Down - } to SceneFamilies.Home, + Swipe.Up to SceneFamilies.Home, Back to SceneFamilies.Home, ) ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt index 6d63d26d4300..313cb30d84ff 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Handler import android.os.Looper import android.service.quicksettings.Tile +import androidx.annotation.DrawableRes import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import androidx.lifecycle.repeatOnLifecycle @@ -98,7 +99,7 @@ constructor( override fun newTileState(): QSTile.State { return QSTile.State().apply { label = mContext.getString(R.string.quick_settings_modes_label) - icon = ResourceIcon.get(R.drawable.qs_dnd_icon_off) + icon = ResourceIcon.get(ICON_RES_ID) state = Tile.STATE_INACTIVE } } @@ -116,7 +117,7 @@ constructor( state?.apply { this.state = tileState.activationState.legacyState val tileStateIcon = tileState.icon() - icon = tileStateIcon?.asQSTileIcon() ?: ResourceIcon.get(R.drawable.qs_dnd_icon_off) + icon = tileStateIcon?.asQSTileIcon() ?: ResourceIcon.get(ICON_RES_ID) label = tileLabel secondaryLabel = tileState.secondaryLabel contentDescription = tileState.contentDescription @@ -127,5 +128,6 @@ constructor( companion object { const val TILE_SPEC = "dnd" + @DrawableRes val ICON_RES_ID = com.android.internal.R.drawable.ic_zen_priority_modes } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index 664951d199a7..6173091b3b99 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -55,7 +55,7 @@ constructor( fun tileData() = zenModeInteractor.activeModes .map { activeModes -> - val modesIconResId = R.drawable.qs_dnd_icon_off + val modesIconResId = com.android.internal.R.drawable.ic_zen_priority_modes if (usesModeIcons()) { val mainModeDrawable = activeModes.mainMode?.icon?.drawable diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt index 93bf73fbfae5..a264f5142293 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt @@ -17,16 +17,24 @@ package com.android.systemui.qs.ui.viewmodel import androidx.lifecycle.LifecycleOwner +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch /** * Models UI state needed for rendering the content of the quick settings scene. @@ -43,7 +51,9 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, val mediaCarouselInteractor: MediaCarouselInteractor, -) { + private val shadeInteractor: ShadeInteractor, + private val sceneInteractor: SceneInteractor, +) : ExclusiveActivatable() { val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasAnyMediaOrRecommendation @@ -56,6 +66,19 @@ constructor( return footerActionsViewModelFactory.create(lifecycleOwner) } + override suspend fun onActivated(): Nothing { + coroutineScope { + launch { + shadeInteractor.shadeMode.collect { shadeMode -> + if (shadeMode == ShadeMode.Split) { + sceneInteractor.snapToScene(Scenes.Shade, "Unfold while on QS") + } + } + } + awaitCancellation() + } + } + @AssistedFactory interface Factory { fun create(): QuickSettingsSceneContentViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt new file mode 100644 index 000000000000..9538392b845f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 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.ui.viewmodel + +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Models the UI state for the user actions for navigating to other scenes or overlays. */ +class QuickSettingsShadeOverlayActionsViewModel @AssistedInject constructor() : + SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + setActions( + buildMap { + put(Swipe.Up, UserActionResult.HideOverlay(Overlays.QuickSettingsShade)) + put(Back, UserActionResult.HideOverlay(Overlays.QuickSettingsShade)) + } + ) + } + + @AssistedFactory + interface Factory { + fun create(): QuickSettingsShadeOverlayActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt new file mode 100644 index 000000000000..3b97d820e6a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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.ui.viewmodel + +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Models UI state used to render the content of the quick settings shade overlay. + * + * Different from [QuickSettingsShadeOverlayActionsViewModel], which only models user actions that + * can be performed to navigate to other scenes. + */ +class QuickSettingsShadeOverlayContentViewModel +@AssistedInject +constructor( + val sceneInteractor: SceneInteractor, + val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, + val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, +) { + fun onScrimClicked() { + sceneInteractor.hideOverlay( + overlay = Overlays.QuickSettingsShade, + loggingReason = "Shade scrim clicked", + ) + } + + @AssistedFactory + interface Factory { + fun create(): QuickSettingsShadeOverlayContentViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt index 9956a46d4701..9690aabdba81 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.qs.ui.viewmodel import com.android.compose.animation.scene.Back @@ -24,11 +22,8 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.shared.model.ShadeAlignment import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.map /** @@ -40,7 +35,6 @@ import kotlinx.coroutines.flow.map class QuickSettingsShadeSceneActionsViewModel @AssistedInject constructor( - private val shadeInteractor: ShadeInteractor, val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, ) : SceneActionsViewModel() { @@ -48,14 +42,7 @@ constructor( quickSettingsContainerViewModel.editModeViewModel.isEditing .map { editing -> buildMap { - put( - if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) { - Swipe.Up - } else { - Swipe.Down - }, - UserActionResult(SceneFamilies.Home) - ) + put(Swipe.Up, UserActionResult(SceneFamilies.Home)) if (!editing) { put(Back, UserActionResult(SceneFamilies.Home)) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt index 924a93923b3a..518582843401 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt @@ -14,14 +14,10 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.qs.ui.viewmodel -import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.ExperimentalCoroutinesApi /** * Models UI state used to render the content of the quick settings shade scene. @@ -32,7 +28,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi class QuickSettingsShadeSceneContentViewModel @AssistedInject constructor( - val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, ) { diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index fe5cbb18f046..000781acec58 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -894,11 +894,21 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis return; } mHandler.removeCallbacks(mConnectionRunnable); + + // Avoid creating TouchInteractionService because the System user in HSUM mode does not + // interact with UI elements + UserHandle currentUser = UserHandle.of(mUserTracker.getUserId()); + if (UserManager.isHeadlessSystemUserMode() && currentUser.isSystem()) { + Log.w(TAG_OPS, + "Skipping connection to TouchInteractionService for the System user in HSUM " + + "mode."); + return; + } try { mBound = mContext.bindServiceAsUser(mQuickStepIntent, mOverviewServiceConnection, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, - UserHandle.of(mUserTracker.getUserId())); + currentUser); } catch (SecurityException e) { Log.e(TAG_OPS, "Unable to bind because of security error", e); } diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceState.kt index 14dfcc59f603..b0eaf9f84952 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceState.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/CustomTraceState.kt @@ -21,6 +21,10 @@ import com.android.traceur.PresetTraceConfigs.TraceOptions import com.android.traceur.PresetTraceConfigs.getDefaultConfig import com.android.traceur.TraceConfig +/** + * This class encapsulates the values that go into a customized record issue trace config, part of + * the RecordIssueTile feature. This class stores the last configuration chosen by power users. + */ class CustomTraceState(private val prefs: SharedPreferences) { private var enabledTags: Set<String>? diff --git a/packages/SystemUI/src/com/android/systemui/scene/EmptySceneModule.kt b/packages/SystemUI/src/com/android/systemui/scene/EmptySceneModule.kt index 4c730a03f0a9..7a57fba9bb81 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/EmptySceneModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/EmptySceneModule.kt @@ -16,8 +16,8 @@ package com.android.systemui.scene -import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.scene.ui.composable.Scene import dagger.Module import dagger.Provides import dagger.multibindings.ElementsIntoSet diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index 6e8997346fc4..00944b8d0849 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -27,6 +27,7 @@ import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartabl import com.android.systemui.scene.domain.startable.SceneContainerStartable import com.android.systemui.scene.domain.startable.ScrimStartable import com.android.systemui.scene.domain.startable.StatusBarStartable +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.shared.flag.DualShade @@ -42,8 +43,10 @@ import dagger.multibindings.IntoMap [ EmptySceneModule::class, GoneSceneModule::class, + NotificationsShadeOverlayModule::class, NotificationsShadeSceneModule::class, NotificationsShadeSessionModule::class, + QuickSettingsShadeOverlayModule::class, QuickSettingsSceneModule::class, ShadeSceneModule::class, SceneDomainModule::class, @@ -99,6 +102,11 @@ interface KeyguardlessSceneContainerFrameworkModule { Scenes.Shade.takeUnless { DualShade.isEnabled }, ), initialSceneKey = Scenes.Gone, + overlayKeys = + listOfNotNull( + Overlays.NotificationsShade.takeIf { DualShade.isEnabled }, + Overlays.QuickSettingsShade.takeIf { DualShade.isEnabled }, + ), navigationDistances = mapOf( Scenes.Gone to 0, diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 7d63b4ce0044..4061ad851f57 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -17,7 +17,6 @@ package com.android.systemui.scene import com.android.systemui.CoreStartable -import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlagsModule import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor @@ -28,6 +27,7 @@ import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartabl import com.android.systemui.scene.domain.startable.SceneContainerStartable import com.android.systemui.scene.domain.startable.ScrimStartable import com.android.systemui.scene.domain.startable.StatusBarStartable +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.shared.flag.DualShade @@ -43,13 +43,14 @@ import dagger.multibindings.IntoMap [ BouncerSceneModule::class, CommunalSceneModule::class, - ComposeBouncerFlagsModule::class, EmptySceneModule::class, GoneSceneModule::class, LockscreenSceneModule::class, QuickSettingsSceneModule::class, ShadeSceneModule::class, + QuickSettingsShadeOverlayModule::class, QuickSettingsShadeSceneModule::class, + NotificationsShadeOverlayModule::class, NotificationsShadeSceneModule::class, NotificationsShadeSessionModule::class, SceneDomainModule::class, @@ -108,6 +109,11 @@ interface SceneContainerFrameworkModule { Scenes.Shade.takeUnless { DualShade.isEnabled }, ), initialSceneKey = Scenes.Lockscreen, + overlayKeys = + listOfNotNull( + Overlays.NotificationsShade.takeIf { DualShade.isEnabled }, + Overlays.QuickSettingsShade.takeIf { DualShade.isEnabled }, + ), navigationDistances = mapOf( Scenes.Gone to 0, diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt index c176ccad7e74..2d40845df802 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneBackInteractor.kt @@ -58,29 +58,20 @@ constructor( fun onSceneChange(from: SceneKey, to: SceneKey) { check(from != to) { "from == to, from=${from.debugName}, to=${to.debugName}" } - when (stackOperation(from, to)) { - Clear -> { - _backStack.value = sceneStackOf() - } - Push -> { - _backStack.update { s -> s.push(from) } - } - Pop -> { - _backStack.update { s -> - checkNotNull(s.pop()) { "Cannot pop ${from.debugName} when stack is empty" } - .also { - val popped = s.peek() - check(popped == to) { - "Expected to pop ${to.debugName} but instead popped ${popped?.debugName}" - } - } - } + + _backStack.update { stack -> + when (stackOperation(from, to, stack)) { + null -> stack + Clear -> sceneStackOf() + Push -> stack.push(from) + Pop -> + checkNotNull(stack.pop()) { "Cannot pop ${from.debugName} when stack is empty" } } } logger.logSceneBackStack(backStack.value.asIterable()) } - private fun stackOperation(from: SceneKey, to: SceneKey): StackOperation { + private fun stackOperation(from: SceneKey, to: SceneKey, stack: SceneStack): StackOperation? { val fromDistance = checkNotNull(sceneContainerConfig.navigationDistances[from]) { "No distance mapping for scene \"${from.debugName}\"!" @@ -93,6 +84,7 @@ constructor( return when { toDistance == 0 -> Clear toDistance > fromDistance -> Push + stack.peek() != to -> null toDistance < fromDistance -> Pop else -> error( @@ -103,7 +95,10 @@ constructor( } private sealed interface StackOperation + private data object Clear : StackOperation + private data object Push : StackOperation + private data object Pop : StackOperation } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index a2142b6ce30c..0d24adc30799 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -202,8 +202,8 @@ constructor( } is ObservableTransitionState.Transition -> { when { - transition.toScene == scene -> transition.progress - transition.fromScene == scene -> transition.progress.map { 1f - it } + transition.toContent == scene -> transition.progress + transition.fromContent == scene -> transition.progress.map { 1f - it } else -> flowOf(0f) } } @@ -501,7 +501,7 @@ constructor( } val inMidTransitionFromGone = - (transitionState.value as? ObservableTransitionState.Transition)?.fromScene == + (transitionState.value as? ObservableTransitionState.Transition)?.fromContent == Scenes.Gone val isChangeAllowed = to != Scenes.Gone || diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt index 9c2b992c0de6..e51a8bc65970 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt @@ -81,14 +81,14 @@ constructor( state.currentScene == Scenes.QuickSettingsShade || state.currentScene == Scenes.Lockscreen is ObservableTransitionState.Transition -> - state.toScene == Scenes.Shade || - state.toScene == Scenes.NotificationsShade || - state.toScene == Scenes.QuickSettingsShade || - state.toScene == Scenes.Lockscreen || - state.fromScene == Scenes.Shade || - state.fromScene == Scenes.NotificationsShade || - state.fromScene == Scenes.QuickSettingsShade || - state.fromScene == Scenes.Lockscreen + state.toContent == Scenes.Shade || + state.toContent == Scenes.NotificationsShade || + state.toContent == Scenes.QuickSettingsShade || + state.toContent == Scenes.Lockscreen || + state.fromContent == Scenes.Shade || + state.fromContent == Scenes.NotificationsShade || + state.fromContent == Scenes.QuickSettingsShade || + state.fromContent == Scenes.Lockscreen } } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 7eb48d6a06ff..0a7526a41d65 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -189,7 +189,7 @@ constructor( // current scene when (state) { is ObservableTransitionState.Idle -> state.currentScene - is ObservableTransitionState.Transition -> state.fromScene + is ObservableTransitionState.Transition -> state.fromContent }.let { it == Scenes.Shade || it == Scenes.QuickSettings } } .distinctUntilChanged() @@ -220,7 +220,7 @@ constructor( } } is ObservableTransitionState.Transition -> { - if (state.fromScene == Scenes.Gone) { + if (state.fromContent == Scenes.Gone) { true to "scene transitioning away from Gone" } else { null @@ -351,8 +351,8 @@ constructor( is ObservableTransitionState.Idle -> setOf(transitionState.currentScene) is ObservableTransitionState.Transition -> setOf( - transitionState.fromScene, - transitionState.toScene, + transitionState.fromContent, + transitionState.toContent, ) } val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen) @@ -461,7 +461,8 @@ constructor( sceneInteractor.transitionState.value as? ObservableTransitionState.Transition ?: return@collect if ( - transition.fromScene == Scenes.Gone && transition.toScene == Scenes.Lockscreen + transition.fromContent == Scenes.Gone && + transition.toContent == Scenes.Lockscreen ) { switchToScene( targetSceneKey = Scenes.Gone, @@ -694,8 +695,8 @@ constructor( .filterIsInstance<ObservableTransitionState.Transition>() // Only consider user-initiated (e.g. drags) that go from bouncer to lockscreen. .filter { transition -> - transition.fromScene == Scenes.Bouncer && - transition.toScene == Scenes.Lockscreen && + transition.fromContent == Scenes.Bouncer && + transition.toContent == Scenes.Lockscreen && transition.isInitiatedByUserInput } .flatMapLatest { it.progress } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index aa418e61598c..fb53ddb0ee7a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -78,8 +78,8 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: tag = TAG, level = LogLevel.INFO, messageInitializer = { - str1 = transitionState.fromScene.toString() - str2 = transitionState.toScene.toString() + str1 = transitionState.fromContent.toString() + str2 = transitionState.toContent.toString() }, messagePrinter = { "Scene transition started: $str1 → $str2" }, ) diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt new file mode 100644 index 000000000000..c47a85082032 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 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.scene.shared.model + +import com.android.compose.animation.scene.OverlayKey + +/** + * Keys of all known overlays. + * + * PLEASE KEEP THE KEYS SORTED ALPHABETICALLY. + */ +object Overlays { + /** + * The notifications shade overlay primarily shows a scrollable list of notifications. + * + * It's used only in the dual shade configuration, where there are two separate shades: one for + * notifications (this overlay) and another for [QuickSettingsShade]. + * + * It's not used in the single/accordion configuration (swipe down once to reveal the shade, + * swipe down again the to expand quick settings) or in the "split" shade configuration (on + * large screens or unfolded foldables, where notifications and quick settings are shown + * side-by-side in their own columns). + */ + @JvmField val NotificationsShade = OverlayKey("notifications_shade") + + /** + * The quick settings shade overlay shows the quick settings tiles UI. + * + * It's used only in the dual shade configuration, where there are two separate shades: one for + * quick settings (this overlay) and another for [NotificationsShade]. + * + * It's not used in the single/accordion configuration (swipe down once to reveal the shade, + * swipe down again the to expand quick settings) or in the "split" shade configuration (on + * large screens or unfolded foldables, where notifications and quick settings are shown + * side-by-side in their own columns). + */ + @JvmField val QuickSettingsShade = OverlayKey("quick_settings_shade") +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt index ef5290ffca65..fcf628872e81 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt @@ -54,7 +54,9 @@ object Scenes { * large screens or unfolded foldables, where notifications and quick settings are shown * side-by-side in their own columns). */ - @JvmField val NotificationsShade = SceneKey("notifications_shade") + @Deprecated("The notifications shade scene has been replaced by an overlay") + @JvmField + val NotificationsShade = SceneKey("notifications_shade") /** * The quick settings scene shows the quick setting tiles. @@ -70,7 +72,9 @@ object Scenes { * and one for quick settings, [NotificationsShade] and [QuickSettingsShade] scenes are used * respectively. */ - @JvmField val QuickSettings = SceneKey("quick_settings") + @Deprecated("The quick settings shade scene has been replaced by an overlay") + @JvmField + val QuickSettings = SceneKey("quick_settings") /** * The quick settings shade scene shows the quick setting tiles as an overlay UI. diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt index be954410fd2d..b9f57f2f31d5 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt @@ -27,14 +27,6 @@ object TransitionKeys { /** Reference to the gone/lockscreen to shade transition with split shade enabled. */ val ToSplitShade = TransitionKey("GoneToSplitShade") - /** Reference to a scene transition that can collapse the shade scene instantly. */ - val CollapseShadeInstantly = TransitionKey("CollapseShadeInstantly") - - /** - * Reference to a scene transition that brings up the shade from the bottom instead of the top. - */ - val OpenBottomShade = TransitionKey("OpenBottomShade") - /** * Reference to a scene transition that can collapse the shade scene slightly faster than a * normal collapse would. diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index c1bb6fb57685..8a2e2745264e 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -6,10 +6,10 @@ import android.view.MotionEvent import android.view.View import android.view.WindowInsets import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies -import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.shade.TouchLogger import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index ec6513a99cad..075599b9505f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -44,11 +44,10 @@ import com.android.systemui.lifecycle.setSnapshotBinding import com.android.systemui.lifecycle.viewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSourceDelegator -import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer @@ -187,8 +186,7 @@ object SceneWindowRootViewBinder { ) { SceneContainer( viewModel = viewModel, - sceneByKey = - sceneByKey.mapValues { (_, scene) -> scene as ComposableScene }, + sceneByKey = sceneByKey, overlayByKey = overlayByKey, initialSceneKey = containerConfig.initialSceneKey, dataSourceDelegator = dataSourceDelegator, diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt index 88d4c4f63fdf..7b0e7f4ded58 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneActionsViewModel.kt @@ -16,14 +16,12 @@ package com.android.systemui.scene.ui.viewmodel -import androidx.compose.ui.Alignment import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.shared.model.TransitionKeys.OpenBottomShade import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode @@ -47,38 +45,23 @@ constructor( // zones. shadeMode is ShadeMode.Dual ) { - if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) { - put( - Swipe( - pointerCount = 2, - fromSource = Edge.Bottom, - direction = SwipeDirection.Up, - ), - UserActionResult(SceneFamilies.QuickSettings, OpenBottomShade) - ) - } else { - put( - Swipe( - pointerCount = 2, - fromSource = Edge.Top, - direction = SwipeDirection.Down, - ), - UserActionResult(SceneFamilies.QuickSettings) - ) - } - } - - if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) { - put(Swipe.Up, UserActionResult(SceneFamilies.NotifShade, OpenBottomShade)) - } else { put( - Swipe.Down, - UserActionResult( - SceneFamilies.NotifShade, - ToSplitShade.takeIf { shadeMode is ShadeMode.Split } - ) + Swipe( + pointerCount = 2, + fromSource = Edge.Top, + direction = SwipeDirection.Down, + ), + UserActionResult(SceneFamilies.QuickSettings) ) } + + put( + Swipe.Down, + UserActionResult( + SceneFamilies.NotifShade, + ToSplitShade.takeIf { shadeMode is ShadeMode.Split } + ) + ) } } .collect { setActions(it) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt index 368e4fa06a26..076613005959 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.asStateFlow * need to worry about resetting the value of [actions] when the view-model is deactivated/canceled, * this base class takes care of it. */ +// TODO(b/363206563): Rename to UserActionsViewModel. abstract class SceneActionsViewModel : ExclusiveActivatable() { private val _actions = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt index 474afa8bcb9d..56afb79c40d4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt @@ -9,7 +9,6 @@ import android.view.ViewGroup.MarginLayoutParams import android.view.ViewTreeObserver import android.view.animation.AccelerateDecelerateInterpolator import androidx.constraintlayout.widget.Guideline -import com.android.systemui.Flags.screenshotPrivateProfileBehaviorFix import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R import com.android.systemui.screenshot.message.ProfileMessageController @@ -49,44 +48,19 @@ constructor( } fun onScreenshotTaken(screenshot: ScreenshotData) { - if (screenshotPrivateProfileBehaviorFix()) { - mainScope.launch { - val profileData = profileMessageController.onScreenshotTaken(screenshot.userHandle) - var notifiedApps: List<CharSequence> = - screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) - - // If profile first run needs to show, bias towards that, otherwise show screenshot - // detection notification if needed. - if (profileData != null) { - workProfileFirstRunView.visibility = View.VISIBLE - detectionNoticeView.visibility = View.GONE - profileMessageController.bindView(workProfileFirstRunView, profileData) { - animateOutMessageContainer() - } - animateInMessageContainer() - } else if (notifiedApps.isNotEmpty()) { - detectionNoticeView.visibility = View.VISIBLE - workProfileFirstRunView.visibility = View.GONE - screenshotDetectionController.populateView(detectionNoticeView, notifiedApps) - animateInMessageContainer() - } - } - } else { - val workProfileData = - workProfileMessageController.onScreenshotTaken(screenshot.userHandle) + mainScope.launch { + val profileData = profileMessageController.onScreenshotTaken(screenshot.userHandle) var notifiedApps: List<CharSequence> = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot) - // If work profile first run needs to show, bias towards that, otherwise show screenshot + // If profile first run needs to show, bias towards that, otherwise show screenshot // detection notification if needed. - if (workProfileData != null) { + if (profileData != null) { workProfileFirstRunView.visibility = View.VISIBLE detectionNoticeView.visibility = View.GONE - workProfileMessageController.populateView( - workProfileFirstRunView, - workProfileData, - this::animateOutMessageContainer - ) + profileMessageController.bindView(workProfileFirstRunView, profileData) { + animateOutMessageContainer() + } animateInMessageContainer() } else if (notifiedApps.isNotEmpty()) { detectionNoticeView.visibility = View.VISIBLE diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt deleted file mode 100644 index 922997d08c25..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt +++ /dev/null @@ -1,62 +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.screenshot - -import android.util.Log -import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE - -/** Implementation of [ScreenshotRequestProcessor] */ -class RequestProcessor( - private val capture: ImageCapture, - private val policy: ScreenshotPolicy, -) : ScreenshotRequestProcessor { - - override suspend fun process(screenshot: ScreenshotData): ScreenshotData { - var result = screenshot - - // Apply work profile screenshots policy: - // - // If the focused app belongs to a work profile, transforms a full screen - // (or partial) screenshot request to a task snapshot (provided image) screenshot. - - // Whenever displayContentInfo is fetched, the topComponent is also populated - // regardless of the managed profile status. - - if (screenshot.type != TAKE_SCREENSHOT_PROVIDED_IMAGE) { - val info = policy.findPrimaryContent(screenshot.displayId) - Log.d(TAG, "findPrimaryContent: $info") - result.taskId = info.taskId - result.topComponent = info.component - result.userHandle = info.user - - if (policy.isManagedProfile(info.user.identifier)) { - val image = - capture.captureTask(info.taskId) - ?: throw RequestProcessorException("Task snapshot returned a null Bitmap!") - - // Provide the task snapshot as the screenshot - result.type = TAKE_SCREENSHOT_PROVIDED_IMAGE - result.bitmap = image - result.screenBounds = info.bounds - } - } - - return result - } -} - -private const val TAG = "RequestProcessor" diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt index 3ad4075a2b89..ee1008d26414 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt @@ -27,5 +27,5 @@ fun interface ScreenshotRequestProcessor { suspend fun process(original: ScreenshotData): ScreenshotData } -/** Exception thrown by [RequestProcessor] if something goes wrong. */ +/** Exception thrown by [ScreenshotRequestProcessor] if something goes wrong. */ class RequestProcessorException(message: String) : IllegalStateException(message) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt index 44f767aa321e..2cb9fe7f1a9d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt @@ -19,14 +19,11 @@ package com.android.systemui.screenshot.policy import android.content.ComponentName import android.content.Context import android.os.Process -import com.android.systemui.Flags.screenshotPrivateProfileBehaviorFix import com.android.systemui.SystemUIService import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.screenshot.ImageCapture -import com.android.systemui.screenshot.RequestProcessor -import com.android.systemui.screenshot.ScreenshotPolicy import com.android.systemui.screenshot.ScreenshotRequestProcessor import com.android.systemui.screenshot.data.repository.DisplayContentRepository import com.android.systemui.screenshot.data.repository.DisplayContentRepositoryImpl @@ -68,23 +65,18 @@ interface ScreenshotPolicyModule { @Application context: Context, @Background background: CoroutineDispatcher, imageCapture: ImageCapture, - policyProvider: Provider<ScreenshotPolicy>, - displayContentRepoProvider: Provider<DisplayContentRepository>, + displayContentRepo: DisplayContentRepository, policyListProvider: Provider<List<CapturePolicy>>, ): ScreenshotRequestProcessor { - return if (screenshotPrivateProfileBehaviorFix()) { - PolicyRequestProcessor( - background = background, - capture = imageCapture, - displayTasks = displayContentRepoProvider.get(), - policies = policyListProvider.get(), - defaultOwner = Process.myUserHandle(), - defaultComponent = - ComponentName(context.packageName, SystemUIService::class.java.toString()) - ) - } else { - RequestProcessor(imageCapture, policyProvider.get()) - } + return PolicyRequestProcessor( + background = background, + capture = imageCapture, + displayTasks = displayContentRepo, + policies = policyListProvider.get(), + defaultOwner = Process.myUserHandle(), + defaultComponent = + ComponentName(context.packageName, SystemUIService::class.java.toString()) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt index e7ee961e1888..edff4bfa2d14 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/DisplayTracker.kt @@ -17,6 +17,7 @@ package com.android.systemui.settings import android.view.Display +import com.android.systemui.util.annotations.WeaklyReferencedCallback import java.util.concurrent.Executor /** @@ -52,6 +53,7 @@ interface DisplayTracker { fun getDisplay(displayId: Int): Display /** Ćallback for notifying of changes. */ + @WeaklyReferencedCallback interface Callback { /** Notifies that a display has been added. */ diff --git a/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java index 05f19ef2f043..b9f9b929d962 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java +++ b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java @@ -16,7 +16,6 @@ package com.android.systemui.settings; -import android.app.ActivityManager; import android.app.IActivityManager; import android.content.Context; import android.hardware.display.DisplayManager; @@ -67,7 +66,7 @@ public abstract class MultiUserUtilsModule { @Background CoroutineDispatcher backgroundDispatcher, @Background Handler handler ) { - int startingUser = ActivityManager.getCurrentUser(); + int startingUser = userManager.getBootUser().getIdentifier(); UserTrackerImpl tracker = new UserTrackerImpl(context, featureFlagsProvider, userManager, iActivityManager, dumpManager, appScope, backgroundDispatcher, handler); tracker.initialize(startingUser); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 34c0cb7c7a31..830649be2a98 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -1005,7 +1005,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum // When expanding QS, let's authenticate the user if possible, // this will speed up notification actions. if (height == 0 && !mKeyguardStateController.canDismissLockScreen()) { - mDeviceEntryFaceAuthInteractor.onQsExpansionStarted(); + mDeviceEntryFaceAuthInteractor.onShadeExpansionStarted(); } } @@ -1063,13 +1063,17 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY); setClippingBounds(); - if (mSplitShadeEnabled) { - // 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); + if (!SceneContainerFlag.isEnabled()) { + if (mSplitShadeEnabled) { + // 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); + } } mDepthController.setQsPanelExpansion(qsExpansionFraction); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 23e2620ac6d6..5d03a28e7f09 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade import android.view.MotionEvent -import androidx.compose.ui.Alignment import com.android.systemui.assist.AssistManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -25,7 +24,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.shared.model.TransitionKeys.OpenBottomShade import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse import com.android.systemui.shade.ShadeController.ShadeVisibilityListener import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -177,7 +175,6 @@ constructor( sceneInteractor.changeScene( SceneFamilies.NotifShade, "ShadeController.animateExpandShade", - OpenBottomShade.takeIf { shadeInteractor.shadeAlignment == Alignment.BottomEnd } ) } @@ -185,7 +182,6 @@ constructor( sceneInteractor.changeScene( SceneFamilies.QuickSettings, "ShadeController.animateExpandQs", - OpenBottomShade.takeIf { shadeInteractor.shadeAlignment == Alignment.BottomEnd } ) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index 018144b8a704..fc8a59395b14 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -37,10 +37,10 @@ import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.scene.ui.view.SceneWindowRootView import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt index a4fed873362b..193056c19d4e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -18,7 +18,6 @@ package com.android.systemui.shade.data.repository import android.content.Context import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -103,9 +102,6 @@ interface ShadeRepository { @Deprecated("Use ShadeInteractor.isQsBypassingShade instead") val legacyExpandImmediate: StateFlow<Boolean> - /** Whether dual shade should be aligned to the bottom (true) or to the top (false). */ - val isDualShadeAlignedToBottom: Boolean - /** * Whether the shade layout should be wide (true) or narrow (false). * @@ -238,9 +234,6 @@ class ShadeRepositoryImpl @Inject constructor(@Application applicationContext: C private val _isShadeLayoutWide = MutableStateFlow(false) override val isShadeLayoutWide: StateFlow<Boolean> = _isShadeLayoutWide.asStateFlow() - override val isDualShadeAlignedToBottom = - applicationContext.resources.getBoolean(R.bool.config_dualShadeAlignedToBottom) - override fun setShadeLayoutWide(isShadeLayoutWide: Boolean) { _isShadeLayoutWide.value = isShadeLayoutWide } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt index 79a94a51768c..8467185b4239 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorSceneContainerImpl.kt @@ -49,10 +49,10 @@ constructor( is ObservableTransitionState.Idle -> flowOf(false) is ObservableTransitionState.Transition -> if ( - (state.fromScene == Scenes.Shade && - state.toScene != Scenes.QuickSettings) || - (state.fromScene == Scenes.QuickSettings && - state.toScene != Scenes.Shade) + (state.fromContent == Scenes.Shade && + state.toContent != Scenes.QuickSettings) || + (state.fromContent == Scenes.QuickSettings && + state.toContent != Scenes.Shade) ) { state.isUserInputOngoing.map { !it } } else { @@ -71,10 +71,10 @@ constructor( is ObservableTransitionState.Transition -> if ( state.isInitiatedByUserInput && - (state.fromScene == Scenes.Shade || - state.toScene == Scenes.Shade || - state.fromScene == Scenes.QuickSettings || - state.toScene == Scenes.QuickSettings) + (state.fromContent == Scenes.Shade || + state.toContent == Scenes.Shade || + state.fromContent == Scenes.QuickSettings || + state.toContent == Scenes.QuickSettings) ) { state.isUserInputOngoing.map { !it } } else { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 45f359efbb7a..73e86a2be4aa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade.domain.interactor -import com.android.systemui.shade.shared.model.ShadeAlignment import com.android.systemui.shade.shared.model.ShadeMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -70,9 +69,6 @@ interface ShadeInteractor : BaseShadeInteractor { * wide as the entire screen. */ val isShadeLayoutWide: StateFlow<Boolean> - - /** How to align the shade content. */ - val shadeAlignment: ShadeAlignment } /** ShadeInteractor methods with implementations that differ between non-empty impls. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index e77aca93aeca..d51fd28d5458 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade.domain.interactor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.shade.shared.model.ShadeAlignment import com.android.systemui.shade.shared.model.ShadeMode import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -48,5 +47,4 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single) override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean - override val shadeAlignment: ShadeAlignment = ShadeAlignment.Top } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index d64b21f2254f..3552092d24e7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -26,7 +26,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeAlignment import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.DozeParameters @@ -114,15 +113,6 @@ constructor( initialValue = determineShadeMode(isShadeLayoutWide.value) ) - override val shadeAlignment: ShadeAlignment - get() { - return if (shadeRepository.isDualShadeAlignedToBottom) { - ShadeAlignment.Bottom - } else { - ShadeAlignment.Top - } - } - override val isExpandToQsEnabled: Flow<Boolean> = combine( disableFlagsRepository.disableFlags, diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index 6a21531d9c06..e84cfa51dd67 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -90,8 +90,8 @@ constructor( when (state) { is ObservableTransitionState.Idle -> false is ObservableTransitionState.Transition -> - state.toScene == quickSettingsScene && - state.fromScene != notificationsScene + state.toContent == quickSettingsScene && + state.fromContent != notificationsScene } } .distinctUntilChanged() @@ -99,14 +99,17 @@ constructor( .distinctUntilChanged() override val isQsFullscreen: Flow<Boolean> = - sceneInteractor - .resolveSceneFamily(SceneFamilies.QuickSettings) - .flatMapLatestConflated { quickSettingsScene -> + combine( + shadeRepository.isShadeLayoutWide, + sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings), + ::Pair + ) + .flatMapLatestConflated { (isShadeLayoutWide, quickSettingsScene) -> sceneInteractor.transitionState .map { state -> when (state) { is ObservableTransitionState.Idle -> - state.currentScene == quickSettingsScene + !isShadeLayoutWide && state.currentScene == quickSettingsScene is ObservableTransitionState.Transition -> false } } @@ -147,9 +150,9 @@ constructor( flowOf(0f) } is ObservableTransitionState.Transition -> - if (state.toScene == resolvedSceneKey) { + if (state.toContent == resolvedSceneKey) { state.progress - } else if (state.fromScene == resolvedSceneKey) { + } else if (state.fromContent == resolvedSceneKey) { state.progress.map { progress -> 1 - progress } } else { flowOf(0f) @@ -172,8 +175,8 @@ constructor( is ObservableTransitionState.Transition -> sceneInteractor.resolveSceneFamily(sceneKey).map { resolvedSceneKey -> state.isInitiatedByUserInput && - (state.toScene == resolvedSceneKey || - state.fromScene == resolvedSceneKey) + (state.toContent == resolvedSceneKey || + state.fromContent == resolvedSceneKey) } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt deleted file mode 100644 index abf1f4cb8b85..000000000000 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2024 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.shade.ui.viewmodel - -import com.android.compose.animation.scene.SceneKey -import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -/** - * Models UI state and handles user input for the overlay shade UI, which shows a shade as an - * overlay on top of another scene UI. - */ -class OverlayShadeViewModel -@AssistedInject -constructor( - private val sceneInteractor: SceneInteractor, - shadeInteractor: ShadeInteractor, -) : ExclusiveActivatable() { - private val _backgroundScene = MutableStateFlow(Scenes.Lockscreen) - /** The scene to show in the background when the overlay shade is open. */ - val backgroundScene: StateFlow<SceneKey> = _backgroundScene.asStateFlow() - - /** Dictates the alignment of the overlay shade panel on the screen. */ - val panelAlignment = shadeInteractor.shadeAlignment - - override suspend fun onActivated(): Nothing { - sceneInteractor.resolveSceneFamily(SceneFamilies.Home).collect { sceneKey -> - _backgroundScene.value = sceneKey - } - awaitCancellation() - } - - /** Notifies that the user has clicked the semi-transparent background scrim. */ - fun onScrimClicked() { - sceneInteractor.changeScene( - toScene = SceneFamilies.Home, - loggingReason = "Shade scrim clicked", - ) - } - - @AssistedFactory - interface Factory { - fun create(): OverlayShadeViewModel - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index 2b44c2f9ea7f..87f360eb9712 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -415,8 +415,8 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi if (!levelEquals) { setImageLevel(icon.iconLevel); } - if (usesModeIcons()) { - setScaleType(icon.shape == Shape.FIXED_SPACE ? ScaleType.FIT_CENTER : ScaleType.CENTER); + if (usesModeIcons() && icon.shape == Shape.FIXED_SPACE) { + setScaleType(ScaleType.FIT_CENTER); } if (!visibilityEquals) { setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt index 84ccaec62867..cce9a1624d51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt @@ -42,9 +42,9 @@ import kotlinx.coroutines.flow.asStateFlow * * Example adb commands: * - * To show a chip with the SysUI icon and custom text: + * To show a chip with the SysUI icon and custom text and color: * ``` - * adb shell cmd statusbar demo-ron -p com.android.systemui -t 10min + * adb shell cmd statusbar demo-ron -p com.android.systemui -t 10min -c "\\#434343" * ``` * * To hide the chip: @@ -87,6 +87,17 @@ constructor( valueParser = Type.String, ) + private val backgroundColor: Int? by + param( + longName = "color", + shortName = "c", + description = + "The color to show as the chip background color. " + + "You can either just write a basic color like 'red' or 'green', " + + "or you can include a #RRGGBB string in this format: \"\\\\#434343\".", + valueParser = Type.Color, + ) + private val hide by flag( longName = "hide", @@ -119,21 +130,26 @@ constructor( return } + val colors = + if (backgroundColor != null) { + ColorsModel.Custom(backgroundColorInt = backgroundColor!!) + } else { + ColorsModel.Themed + } + val currentText = text if (currentText != null) { _chip.value = OngoingActivityChipModel.Shown.Text( icon = appIcon, - // TODO(b/361346412): Include a demo with a custom color theme. - colors = ColorsModel.Themed, + colors = colors, text = currentText, ) } else { _chip.value = OngoingActivityChipModel.Shown.Timer( icon = appIcon, - // TODO(b/361346412): Include a demo with a custom color theme. - colors = ColorsModel.Themed, + colors = colors, startTimeMs = systemClock.elapsedRealtime(), onClickListener = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt index 8a5165d8df7b..4b0fc5ab6059 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/ColorsModel.kt @@ -39,6 +39,24 @@ sealed interface ColorsModel { Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary) } + /** + * The chip should have the given background color, and text color that matches dark/light + * theme. + */ + data class Custom(val backgroundColorInt: Int) : ColorsModel { + override fun background(context: Context): ColorStateList = + ColorStateList.valueOf(backgroundColorInt) + + // TODO(b/361346412): When dark theme changes, the chip should automatically re-render with + // the right text color. Right now, it has the right text color when the chip is first + // created but the color doesn't update if dark theme changes. + override fun text(context: Context) = + Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.materialColorOnSurface, + ) + } + /** The chip should have a red background with white text. */ data object Red : ColorsModel { override fun background(context: Context): ColorStateList { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ValueParser.kt b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ValueParser.kt index 01083d9a7907..412c8c629e0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ValueParser.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/commandline/ValueParser.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.commandline +import androidx.core.graphics.toColorInt import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -164,10 +165,23 @@ private val parseFloat: ValueParser<Float> = ValueParser { value -> ?: Result.failure(ArgParseError("Failed to parse $value as a float")) } +// See https://developer.android.com/reference/android/graphics/Color#parseColor(java.lang.String) +// for the supported formats of the color string. tl;dr: #RRGGBB, #AARRGGBB, or a basic color name +// like "red" or "green". For the RRGGBB values, the `#` needs to be escaped. Use `"\\#RRGGBB"` in +// the command to escape the `#` correctly. +private val parseColor: ValueParser<Int> = ValueParser { value -> + try { + Result.success(value.toColorInt()) + } catch (e: IllegalArgumentException) { + Result.failure(ArgParseError("Failed to parse $value as a color: $e")) + } +} + /** Default parsers that can be use as-is, or [map]ped to another type */ object Type { val Boolean = parseBoolean val Int = parseInt val Float = parseFloat val String = parseString + val Color = parseColor } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 2f3719a34ac8..1431b28bf794 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -533,8 +533,15 @@ public class AmbientState implements Dumpable { if (mDozeAmount == 1.0f && !isPulseExpanding()) { return mShelf.getHeight(); } - int height = (int) Math.max(mLayoutMinHeight, - Math.min(mLayoutHeight, mContentHeight) - mTopPadding); + int height; + if (SceneContainerFlag.isEnabled()) { + // TODO(b/192348384): This is probably incorrect as mContentHeight is not up to date. + // Consider removing usages of getInnerHeight in flexiglass if possible. + height = (int) Math.min(mLayoutHeight, mContentHeight) - mTopPadding; + } else { + height = (int) Math.max(mLayoutMinHeight, + Math.min(mLayoutHeight, mContentHeight) - mTopPadding); + } if (ignorePulseHeight) { return height; } @@ -571,6 +578,7 @@ public class AmbientState implements Dumpable { } public void setLayoutMinHeight(int layoutMinHeight) { + SceneContainerFlag.assertInLegacyMode(); mLayoutMinHeight = layoutMinHeight; } 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 64d71240073f..b9628e95c436 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 @@ -789,7 +789,6 @@ public class NotificationStackScrollLayout private void onJustBeforeDraw() { if (SceneContainerFlag.isEnabled()) { if (mChildrenUpdateRequested) { - updateForcedScroll(); updateChildren(); mChildrenUpdateRequested = false; } @@ -1309,8 +1308,10 @@ public class NotificationStackScrollLayout } private void updateAlgorithmLayoutMinHeight() { - mAmbientState.setLayoutMinHeight(mQsFullScreen || isHeadsUpTransition() - ? getLayoutMinHeightInternal() : 0); + if (!SceneContainerFlag.isEnabled()) { + mAmbientState.setLayoutMinHeight(mQsFullScreen || isHeadsUpTransition() + ? getLayoutMinHeightInternal() : 0); + } } /** @@ -1998,7 +1999,8 @@ public class NotificationStackScrollLayout } public void lockScrollTo(View v) { - if (mForcedScroll == v) { + // NSSL shouldn't handle scrolling with SceneContainer enabled. + if (mForcedScroll == v || SceneContainerFlag.isEnabled()) { return; } mForcedScroll = v; @@ -2006,6 +2008,10 @@ public class NotificationStackScrollLayout } public boolean scrollTo(View v) { + // NSSL shouldn't handle scrolling with SceneContainer enabled. + if (SceneContainerFlag.isEnabled()) { + return false; + } ExpandableView expandableView = (ExpandableView) v; int positionInLinearLayout = getPositionInLinearLayout(v); int targetScroll = targetScrollForView(expandableView, positionInLinearLayout); @@ -2027,6 +2033,7 @@ public class NotificationStackScrollLayout * the IME. */ private int targetScrollForView(ExpandableView v, int positionInLinearLayout) { + SceneContainerFlag.assertInLegacyMode(); return positionInLinearLayout + v.getIntrinsicHeight() + getImeInset() - getHeight() + ((!isExpanded() && isPinnedHeadsUp(v)) ? mHeadsUpInset : getTopPadding()); @@ -2624,6 +2631,9 @@ public class NotificationStackScrollLayout } private void updateScrollability() { + if (SceneContainerFlag.isEnabled()) { + return; + } boolean scrollable = !mQsFullScreen && getScrollRange() > 0; if (scrollable != mScrollable) { mScrollable = scrollable; @@ -2633,6 +2643,7 @@ public class NotificationStackScrollLayout } private void updateForwardAndBackwardScrollability() { + SceneContainerFlag.assertInLegacyMode(); boolean forwardScrollable = mScrollable && !mScrollAdapter.isScrolledToBottom(); boolean backwardsScrollable = mScrollable && !mScrollAdapter.isScrolledToTop(); boolean changed = forwardScrollable != mForwardScrollable @@ -2790,6 +2801,7 @@ public class NotificationStackScrollLayout } private int getLayoutMinHeightInternal() { + SceneContainerFlag.assertInLegacyMode(); if (isHeadsUpTransition()) { ExpandableNotificationRow trackedHeadsUpRow = mAmbientState.getTrackedHeadsUpRow(); if (trackedHeadsUpRow.isAboveShelf()) { @@ -4172,6 +4184,11 @@ public class NotificationStackScrollLayout */ @Override public boolean performAccessibilityActionInternal(int action, Bundle arguments) { + // Don't handle scroll accessibility events from the NSSL, when SceneContainer enabled. + if (SceneContainerFlag.isEnabled()) { + return super.performAccessibilityActionInternal(action, arguments); + } + if (super.performAccessibilityActionInternal(action, arguments)) { return true; } @@ -4933,6 +4950,11 @@ public class NotificationStackScrollLayout @Override public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { super.onInitializeAccessibilityEventInternal(event); + // Don't handle scroll accessibility events from the NSSL, when SceneContainer enabled. + if (SceneContainerFlag.isEnabled()) { + return; + } + event.setScrollable(mScrollable); event.setMaxScrollX(mScrollX); event.setScrollY(mOwnScrollY); @@ -4942,6 +4964,11 @@ public class NotificationStackScrollLayout @Override public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfoInternal(info); + // Don't handle scroll accessibility events from the NSSL, when SceneContainer enabled. + if (SceneContainerFlag.isEnabled()) { + return; + } + if (mScrollable) { info.setScrollable(true); if (mBackwardScrollable) { @@ -5127,10 +5154,12 @@ public class NotificationStackScrollLayout } boolean isQsFullScreen() { + SceneContainerFlag.assertInLegacyMode(); return mQsFullScreen; } public void setQsExpansionFraction(float qsExpansionFraction) { + SceneContainerFlag.assertInLegacyMode(); boolean footerAffected = mQsExpansionFraction != qsExpansionFraction && (mQsExpansionFraction == 1 || qsExpansionFraction == 1); mQsExpansionFraction = qsExpansionFraction; @@ -5174,6 +5203,7 @@ public class NotificationStackScrollLayout } private void updateOnScrollChange() { + SceneContainerFlag.assertInLegacyMode(); if (mScrollListener != null) { mScrollListener.accept(mOwnScrollY); } 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 e112c99e19b3..bcdc3bc583ad 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 @@ -1275,6 +1275,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setQsExpansionFraction(float expansionFraction) { + SceneContainerFlag.assertInLegacyMode(); mView.setQsExpansionFraction(expansionFraction); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java index 56ea00cf0954..7ef1e416aa19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java @@ -22,6 +22,7 @@ import android.os.UserManager; import androidx.annotation.NonNull; +import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.UserTracker; @@ -43,17 +44,20 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { private final UserManager mUserManager; private final UserTracker mUserTracker; private final LinkedList<UserInfo> mProfiles; + private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private boolean mListening; private int mCurrentUser; @Inject public ManagedProfileControllerImpl(Context context, @Main Executor mainExecutor, - UserTracker userTracker, UserManager userManager) { + UserTracker userTracker, UserManager userManager, + KeyguardUpdateMonitor keyguardUpdateMonitor) { mContext = context; mMainExecutor = mainExecutor; mUserManager = userManager; mUserTracker = userTracker; + mKeyguardUpdateMonitor = keyguardUpdateMonitor; mProfiles = new LinkedList<>(); } @@ -80,6 +84,7 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { StatusBarManager statusBarManager = (StatusBarManager) mContext .getSystemService(android.app.Service.STATUS_BAR_SERVICE); statusBarManager.collapsePanels(); + mKeyguardUpdateMonitor.awakenFromDream(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt index 21ec14fc7f03..591d7af44db1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt @@ -403,7 +403,7 @@ interface PolicyModule { tileSpec = TileSpec.create(DND_TILE_SPEC), uiConfig = QSTileUIConfig.Resource( - iconRes = R.drawable.qs_dnd_icon_off, + iconRes = com.android.internal.R.drawable.ic_zen_priority_modes, labelRes = R.string.quick_settings_modes_label, ), instanceId = uiEventLogger.getNewInstanceId(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt index cacb3843866b..0e88f44e0152 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy.ui.dialog.composable +import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -30,8 +31,10 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription @@ -44,12 +47,16 @@ import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewMod @Composable fun ModeTile(viewModel: ModeTileViewModel) { - val tileColor = - if (viewModel.enabled) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.surfaceVariant - val contentColor = - if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary - else MaterialTheme.colorScheme.onSurfaceVariant + val tileColor: Color by + animateColorAsState( + if (viewModel.enabled) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceVariant + ) + val contentColor: Color by + animateColorAsState( + if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurfaceVariant + ) CompositionLocalProvider(LocalContentColor provides contentColor) { Surface( diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt index 4f77cd04f5fa..73728e698c09 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt @@ -75,7 +75,7 @@ constructor( } .map { it ?: AudioOutputDevice.Unknown } .flowOn(backgroundCoroutineContext) - .stateIn(scope, SharingStarted.Eagerly, AudioOutputDevice.Unknown) + .stateIn(scope, SharingStarted.Eagerly, AudioOutputDevice.Unavailable) private fun AudioDeviceInfo.toAudioOutputDevice(): AudioOutputDevice { if ( @@ -120,6 +120,11 @@ constructor( name = name, icon = icon, ) + deviceType == MediaDeviceType.TYPE_CAST_DEVICE -> + AudioOutputDevice.Remote( + name = name, + icon = icon, + ) else -> AudioOutputDevice.BuiltIn( name = name, diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt index ba0b08210799..0e4cac0b2224 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/model/AudioOutputDevice.kt @@ -31,6 +31,12 @@ sealed interface AudioOutputDevice { override val icon: Drawable?, ) : AudioOutputDevice + /** Models a cast audio output device. */ + data class Remote( + override val name: String, + override val icon: Drawable?, + ) : AudioOutputDevice + /** Models a wired audio output device. */ data class Wired( override val name: String, @@ -52,4 +58,16 @@ sealed interface AudioOutputDevice { override val icon: Drawable get() = error("Unsupported for unknown device") } + + /** + * Models a state when current audio output device is not loaded yet or the system failed to + * load it. + */ + data object Unavailable : AudioOutputDevice { + override val name: String + get() = error("Unsupported for unavailable device") + + override val icon: Drawable + get() = error("Unsupported for unavailable device") + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt index a270d5ffa9de..f94cbda49e91 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt @@ -74,34 +74,51 @@ constructor( ) private val currentAudioDevice: Flow<AudioOutputDevice> = - audioOutputInteractor.currentAudioDevice.filter { it !is AudioOutputDevice.Unknown } + audioOutputInteractor.currentAudioDevice.filter { it !is AudioOutputDevice.Unavailable } + /** + * Model for the Media Output component in the Volume Panel. It's guaranteed to have an + * available device if it's loaded. + */ val mediaOutputModel: StateFlow<Result<MediaOutputComponentModel>> = - audioModeInteractor.isOngoingCall - .flatMapLatest { isOngoingCall -> - audioSharingInteractor.isInAudioSharing.flatMapLatest { isInAudioSharing -> - if (isOngoingCall) { - currentAudioDevice.map { - MediaOutputComponentModel.Calling(it, isInAudioSharing) - } - } else { - combine(sessionWithPlaybackState.filterData(), currentAudioDevice) { - sessionWithPlaybackState, - currentAudioDevice -> - if (sessionWithPlaybackState == null) { - MediaOutputComponentModel.Idle(currentAudioDevice, isInAudioSharing) - } else { - MediaOutputComponentModel.MediaSession( - sessionWithPlaybackState.session, - sessionWithPlaybackState.isPlaybackActive, - currentAudioDevice, - isInAudioSharing, - ) - } + combine( + audioSharingInteractor.isInAudioSharing, + audioModeInteractor.isOngoingCall, + currentAudioDevice, + ) { isInAudioSharing, isOngoingCall, currentAudioDevice -> + if (isOngoingCall) { + flowOf( + MediaOutputComponentModel.Calling( + device = currentAudioDevice, + isInAudioSharing = isInAudioSharing, + canOpenAudioSwitcher = false, + ) + ) + } else { + sessionWithPlaybackState.filterData().map { sessionWithPlaybackState -> + if (sessionWithPlaybackState == null) { + MediaOutputComponentModel.Idle( + device = currentAudioDevice, + isInAudioSharing = isInAudioSharing, + canOpenAudioSwitcher = + !isInAudioSharing && + currentAudioDevice !is AudioOutputDevice.Unknown, + ) + } else { + MediaOutputComponentModel.MediaSession( + session = sessionWithPlaybackState.session, + isPlaybackActive = sessionWithPlaybackState.isPlaybackActive, + device = currentAudioDevice, + isInAudioSharing = isInAudioSharing, + canOpenAudioSwitcher = + !isInAudioSharing && + currentAudioDevice !is AudioOutputDevice.Unknown, + ) } } } } + .flatMapLatest { it } .wrapInResult() .stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading()) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index 31a89775e916..aa07cfd26bdb 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -179,9 +179,7 @@ constructor( return MediaDeviceSession( packageName = packageName, sessionToken = sessionToken, - canAdjustVolume = - playbackInfo != null && - playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED, + canAdjustVolume = playbackInfo.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED, appLabel = getApplicationLabel(packageName) ?: return null ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaOutputComponentModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaOutputComponentModel.kt index 220fb2b42eb8..6588b443b0af 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaOutputComponentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaOutputComponentModel.kt @@ -24,11 +24,13 @@ sealed interface MediaOutputComponentModel { val device: AudioOutputDevice val isInAudioSharing: Boolean + val canOpenAudioSwitcher: Boolean /** There is an ongoing call on the device. */ data class Calling( override val device: AudioOutputDevice, override val isInAudioSharing: Boolean, + override val canOpenAudioSwitcher: Boolean, ) : MediaOutputComponentModel /** There is media playing on the device. */ @@ -37,11 +39,13 @@ sealed interface MediaOutputComponentModel { val isPlaybackActive: Boolean, override val device: AudioOutputDevice, override val isInAudioSharing: Boolean, + override val canOpenAudioSwitcher: Boolean, ) : MediaOutputComponentModel /** There is nothing playing on the device. */ data class Idle( override val device: AudioOutputDevice, override val isInAudioSharing: Boolean, + override val canOpenAudioSwitcher: Boolean, ) : MediaOutputComponentModel } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/ConnectedDeviceViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/ConnectedDeviceViewModel.kt index 8ba672d2a15e..42f88b48110c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/ConnectedDeviceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/ConnectedDeviceViewModel.kt @@ -16,11 +16,15 @@ package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel +import com.android.systemui.common.shared.model.Color + /** * Models part of the Media Session Volume Panel component that displays connected device * information. */ data class ConnectedDeviceViewModel( val label: CharSequence, + val labelColor: Color, val deviceName: CharSequence?, + val deviceNameColor: Color, ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index 36b42f28e202..e565de5c55ea 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -75,12 +75,25 @@ constructor( } } ConnectedDeviceViewModel( - label, - if (mediaOutputModel.isInAudioSharing) { - context.getString(R.string.audio_sharing_description) - } else { - mediaOutputModel.device.name - }, + label = label, + labelColor = + Color.Attribute(com.android.internal.R.attr.materialColorOnSurfaceVariant), + deviceName = + if (mediaOutputModel.isInAudioSharing) { + context.getString(R.string.audio_sharing_description) + } else { + mediaOutputModel.device + .takeIf { it !is AudioOutputDevice.Unknown } + ?.name ?: context.getString(R.string.media_seamless_other_device) + }, + deviceNameColor = + if (mediaOutputModel.canOpenAudioSwitcher) { + Color.Attribute(com.android.internal.R.attr.materialColorOnSurface) + } else { + Color.Attribute( + com.android.internal.R.attr.materialColorOnSurfaceVariant + ) + }, ) } .stateIn( @@ -107,19 +120,39 @@ constructor( DeviceIconViewModel.IsPlaying( icon = icon, iconColor = - Color.Attribute(com.android.internal.R.attr.materialColorSurface), + if (mediaOutputModel.canOpenAudioSwitcher) { + Color.Attribute(com.android.internal.R.attr.materialColorSurface) + } else { + Color.Attribute( + com.android.internal.R.attr.materialColorSurfaceContainerHighest + ) + }, backgroundColor = - Color.Attribute(com.android.internal.R.attr.materialColorSecondary), + if (mediaOutputModel.canOpenAudioSwitcher) { + Color.Attribute(com.android.internal.R.attr.materialColorSecondary) + } else { + Color.Attribute(com.android.internal.R.attr.materialColorOutline) + }, ) } else { DeviceIconViewModel.IsNotPlaying( icon = icon, iconColor = - Color.Attribute( - com.android.internal.R.attr.materialColorOnSurfaceVariant - ), + if (mediaOutputModel.canOpenAudioSwitcher) { + Color.Attribute( + com.android.internal.R.attr.materialColorOnSurfaceVariant + ) + } else { + Color.Attribute(com.android.internal.R.attr.materialColorOutline) + }, backgroundColor = - Color.Attribute(com.android.internal.R.attr.materialColorSurface), + if (mediaOutputModel.canOpenAudioSwitcher) { + Color.Attribute(com.android.internal.R.attr.materialColorSurface) + } else { + Color.Attribute( + com.android.internal.R.attr.materialColorSurfaceContainerHighest + ) + }, ) } } @@ -132,7 +165,7 @@ constructor( val enabled: StateFlow<Boolean> = mediaOutputComponentInteractor.mediaOutputModel .filterData() - .map { !it.isInAudioSharing } + .map { it.canOpenAudioSwitcher } .stateIn( coroutineScope, SharingStarted.Eagerly, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt index cfcd6b14010d..56d0bce23f17 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt @@ -63,13 +63,7 @@ constructor( private val changes = MutableSharedFlow<Unit>() private val currentAudioDeviceAttributes: StateFlow<AudioDeviceAttributes?> = audioOutputInteractor.currentAudioDevice - .map { audioDevice -> - if (audioDevice is AudioOutputDevice.Unknown) { - builtinSpeaker - } else { - audioDevice.getAudioDeviceAttributes() - } - } + .map { audioDevice -> audioDevice.getAudioDeviceAttributes() } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), builtinSpeaker) /** @@ -185,7 +179,10 @@ constructor( .firstOrNull { spatializerInteractor.isSpatialAudioAvailable(it) } } } - else -> null + is AudioOutputDevice.Wired -> null + is AudioOutputDevice.Remote -> null + is AudioOutputDevice.Unknown -> builtinSpeaker + is AudioOutputDevice.Unavailable -> builtinSpeaker } } diff --git a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java index 4841c78f6da0..ea213cba9567 100644 --- a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java +++ b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java @@ -23,11 +23,19 @@ import android.service.quickaccesswallet.QuickAccessWalletClient; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.pipeline.shared.TileSpec; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.qs.tiles.QuickAccessWalletTile; +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig; +import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy; +import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig; +import com.android.systemui.res.R; import com.android.systemui.wallet.controller.WalletContextualLocationsService; import com.android.systemui.wallet.ui.WalletActivity; +import java.util.concurrent.Executor; + import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -35,14 +43,14 @@ import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; import dagger.multibindings.StringKey; -import java.util.concurrent.Executor; - /** * Module for injecting classes in Wallet. */ @Module public abstract class WalletModule { + public static final String WALLET_TILE_SPEC = "wallet"; + @Binds @IntoMap @ClassKey(WalletContextualLocationsService.class) @@ -69,4 +77,21 @@ public abstract class WalletModule { @StringKey(QuickAccessWalletTile.TILE_SPEC) public abstract QSTileImpl<?> bindQuickAccessWalletTile( QuickAccessWalletTile quickAccessWalletTile); + + @Provides + @IntoMap + @StringKey(WALLET_TILE_SPEC) + public static QSTileConfig provideQuickAccessWalletTileConfig(QsEventLogger uiEventLogger) { + TileSpec tileSpec = TileSpec.create(WALLET_TILE_SPEC); + return new QSTileConfig( + tileSpec, + new QSTileUIConfig.Resource( + R.drawable.ic_wallet_lockscreen, + R.string.wallet_title + ), + uiEventLogger.getNewInstanceId(), + tileSpec.getSpec(), + QSTilePolicy.NoRestrictions.INSTANCE + ); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java index e724c60bfc54..c0d8be322cd6 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java @@ -16,6 +16,8 @@ package com.android.keyguard; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -27,6 +29,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper.RunWithLooper; import android.view.KeyEvent; @@ -43,9 +46,13 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.haptics.msdl.FakeMSDLPlayer; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.data.model.MSDLToken; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -85,7 +92,11 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { private FakeFeatureFlags mFeatureFlags; @Mock private SelectedUserInteractor mSelectedUserInteractor; + @Mock + private UserActivityNotifier mUserActivityNotifier; private KeyguardAbsKeyInputViewController mKeyguardAbsKeyInputViewController; + private KosmosJavaAdapter mKosmosJavaAdapter = new KosmosJavaAdapter(this); + private final FakeMSDLPlayer mMSDLPlayer = mKosmosJavaAdapter.getMsdlPlayer(); @Before public void setup() { @@ -108,7 +119,8 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { return new KeyguardAbsKeyInputViewController(mAbsKeyInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mFalsingCollector, - mEmergencyButtonController, mFeatureFlags, mSelectedUserInteractor) { + mEmergencyButtonController, mFeatureFlags, mSelectedUserInteractor, mMSDLPlayer, + mUserActivityNotifier) { @Override void resetState() { } @@ -197,4 +209,32 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { verify(mAbsKeyInputView, never()).setPasswordEntryInputEnabled(true); verify(mAbsKeyInputView, never()).setPasswordEntryEnabled(true); } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + public void onPasswordChecked_withMSDLFeedback_withMatch_playsUnlockToken() { + mKeyguardAbsKeyInputViewController.onPasswordChecked(0, true, 100, true); + assertThat(mMSDLPlayer.getLatestTokenPlayed()).isEqualTo(MSDLToken.UNLOCK); + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) + public void onPasswordChecked_withoutMSDLFeedback_withMatch_doesNotPlayToken() { + mKeyguardAbsKeyInputViewController.onPasswordChecked(0, true, 100, true); + assertThat(mMSDLPlayer.getLatestTokenPlayed()).isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + public void onPasswordChecked_withMSDLFeedback_withoutMatch_playsFailureToken() { + mKeyguardAbsKeyInputViewController.onPasswordChecked(0, false, 100, true); + assertThat(mMSDLPlayer.getLatestTokenPlayed()).isEqualTo(MSDLToken.FAILURE); + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) + public void onPasswordChecked_withoutMSDLFeedback_withoutMatch_doesNotPlayToken() { + mKeyguardAbsKeyInputViewController.onPasswordChecked(0, false, 100, true); + assertThat(mMSDLPlayer.getLatestTokenPlayed()).isNull(); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 36d4d122ab11..873bc2c92431 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -103,6 +103,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Mock lateinit var deleteButton: NumPadButton @Mock lateinit var enterButton: View @Mock lateinit var uiEventLogger: UiEventLogger + @Mock lateinit var mUserActivityNotifier: UserActivityNotifier @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> @@ -149,7 +150,9 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { featureFlags, mSelectedUserInteractor, uiEventLogger, - keyguardKeyboardInteractor + keyguardKeyboardInteractor, + null, + mUserActivityNotifier ) } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index 7151c429acf9..f141a4926149 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -69,6 +69,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { @Mock private lateinit var keyguardMessageAreaController: KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier private val updateMonitorCallbackArgumentCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) @@ -101,7 +102,9 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { emergencyButtonController, fakeFeatureFlags, mSelectedUserInteractor, - keyguardKeyboardInteractor + keyguardKeyboardInteractor, + null, + mUserActivityNotifier ) underTest.init() underTest.onViewAttached() diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index acae913459b3..a03c8391fa0f 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -63,6 +63,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { @Mock private lateinit var keyguardMessageAreaController: KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock private lateinit var mUserActivityNotifier: UserActivityNotifier @Before fun setup() { @@ -96,7 +97,9 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { emergencyButtonController, fakeFeatureFlags, mSelectedUserInteractor, - keyguardKeyboardInteractor + keyguardKeyboardInteractor, + null, + mUserActivityNotifier ) underTest.init() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 6047e7d1bf79..4fc41669b2c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -717,13 +717,9 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(confirmHaptics?.hapticFeedbackConstant) .isEqualTo( if (expectConfirmation) HapticFeedbackConstants.NO_HAPTICS - else HapticFeedbackConstants.CONFIRM - ) - assertThat(confirmHaptics?.flag) - .isEqualTo( - if (expectConfirmation) null - else HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + else HapticFeedbackConstants.BIOMETRIC_CONFIRM ) + assertThat(confirmHaptics?.flag).isNull() if (expectConfirmation) { kosmos.promptViewModel.confirmAuthenticated() @@ -731,9 +727,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val confirmedHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) assertThat(confirmedHaptics?.hapticFeedbackConstant) - .isEqualTo(HapticFeedbackConstants.CONFIRM) - assertThat(confirmedHaptics?.flag) - .isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) + .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) + assertThat(confirmedHaptics?.flag).isNull() } @Test @@ -747,9 +742,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val currentHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) assertThat(currentHaptics?.hapticFeedbackConstant) - .isEqualTo(HapticFeedbackConstants.CONFIRM) - assertThat(currentHaptics?.flag) - .isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) + .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) + assertThat(currentHaptics?.flag).isNull() } @Test @@ -757,9 +751,9 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.showTemporaryError("test", "messageAfterError", false) val currentHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(currentHaptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.REJECT) - assertThat(currentHaptics?.flag) - .isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) + assertThat(currentHaptics?.hapticFeedbackConstant) + .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) + assertThat(currentHaptics?.flag).isNull() } // biometricprompt_sfps_fingerprint_authenticating reused across rotations @@ -870,8 +864,9 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa ) val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.REJECT) - assertThat(haptics?.flag).isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) + assertThat(haptics?.hapticFeedbackConstant) + .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) + assertThat(haptics?.flag).isNull() } @Test @@ -901,10 +896,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) if (expectConfirmation) { - assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.REJECT) - assertThat(haptics?.flag).isEqualTo(HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING) + assertThat(haptics?.hapticFeedbackConstant) + .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) + assertThat(haptics?.flag).isNull() } else { - assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.CONFIRM) + assertThat(haptics?.hapticFeedbackConstant) + .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index 4883d1bb9400..40b2a0864a8c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -362,33 +362,33 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { } @Test - fun faceAuthIsRequestedWhenQsExpansionStared() = + fun faceAuthIsRequestedWhenShadeExpansionStarted() = testScope.runTest { underTest.start() - underTest.onQsExpansionStarted() + underTest.onShadeExpansionStarted() runCurrent() assertThat(faceAuthRepository.runningAuthRequest.value) - .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true)) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, false)) } @Test @EnableSceneContainer - fun faceAuthIsRequestedWhenQuickSettingsIsExpandedToTheShade() = + fun faceAuthIsRequestedWhenShadeExpansionIsStarted() = testScope.runTest { underTest.start() faceAuthRepository.canRunFaceAuth.value = true - kosmos.sceneInteractor.snapToScene(toScene = Scenes.QuickSettings, "for-test") + kosmos.sceneInteractor.snapToScene(toScene = Scenes.Lockscreen, "for-test") runCurrent() kosmos.sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = "for-test") kosmos.sceneInteractor.setTransitionState( MutableStateFlow( ObservableTransitionState.Transition( - fromScene = Scenes.QuickSettings, + fromScene = Scenes.Lockscreen, toScene = Scenes.Shade, - currentScene = flowOf(Scenes.QuickSettings), + currentScene = flowOf(Scenes.Lockscreen), progress = MutableStateFlow(0.2f), isInitiatedByUserInput = true, isUserInputOngoing = flowOf(false), @@ -398,25 +398,25 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { runCurrent() assertThat(faceAuthRepository.runningAuthRequest.value) - .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true)) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, false)) } @Test @EnableSceneContainer - fun faceAuthIsRequestedOnlyOnceWhenQuickSettingsIsExpandedToTheShade() = + fun faceAuthIsRequestedOnlyOnceWhenShadeExpansionStarts() = testScope.runTest { underTest.start() faceAuthRepository.canRunFaceAuth.value = true - kosmos.sceneInteractor.snapToScene(toScene = Scenes.QuickSettings, "for-test") + kosmos.sceneInteractor.snapToScene(toScene = Scenes.Lockscreen, "for-test") runCurrent() kosmos.sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = "for-test") kosmos.sceneInteractor.setTransitionState( MutableStateFlow( ObservableTransitionState.Transition( - fromScene = Scenes.QuickSettings, + fromScene = Scenes.Lockscreen, toScene = Scenes.Shade, - currentScene = flowOf(Scenes.QuickSettings), + currentScene = flowOf(Scenes.Lockscreen), progress = MutableStateFlow(0.2f), isInitiatedByUserInput = true, isUserInputOngoing = flowOf(false), @@ -426,16 +426,16 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { runCurrent() assertThat(faceAuthRepository.runningAuthRequest.value) - .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, true)) + .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, false)) faceAuthRepository.runningAuthRequest.value = null // expansion progress shouldn't trigger face auth again kosmos.sceneInteractor.setTransitionState( MutableStateFlow( ObservableTransitionState.Transition( - fromScene = Scenes.QuickSettings, + fromScene = Scenes.Lockscreen, toScene = Scenes.Shade, - currentScene = flowOf(Scenes.QuickSettings), + currentScene = flowOf(Scenes.Lockscreen), progress = MutableStateFlow(0.5f), isInitiatedByUserInput = true, isUserInputOngoing = flowOf(false), diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt index 9d9e5be62351..3ccb989dc65a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt @@ -34,14 +34,16 @@ import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepositoryImpl import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -57,29 +59,28 @@ import org.mockito.Mockito.verifyZeroInteractions @RunWith(AndroidJUnit4::class) class StickyKeysIndicatorViewModelTest : SysuiTestCase() { - private val dispatcher = StandardTestDispatcher() - private val testScope = TestScope(dispatcher) + private val kosmos = testKosmos() + private val dispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope private lateinit var viewModel: StickyKeysIndicatorViewModel private val inputManager = mock<InputManager>() private val keyboardRepository = FakeKeyboardRepository() - private val secureSettings = FakeSettings() + private val secureSettings = kosmos.fakeSettings private val userRepository = Kosmos().fakeUserRepository private val captor = ArgumentCaptor.forClass(InputManager.StickyModifierStateListener::class.java) @Before fun setup() { - val settingsRepository = UserAwareSecureSettingsRepositoryImpl( - secureSettings, - userRepository, - dispatcher - ) - val stickyKeysRepository = StickyKeysRepositoryImpl( - inputManager, - dispatcher, - settingsRepository, - mock<StickyKeysLogger>() - ) + val settingsRepository = + UserAwareSecureSettingsRepositoryImpl(secureSettings, userRepository, dispatcher) + val stickyKeysRepository = + StickyKeysRepositoryImpl( + inputManager, + dispatcher, + settingsRepository, + mock<StickyKeysLogger>() + ) setStickyKeySetting(enabled = false) viewModel = StickyKeysIndicatorViewModel( @@ -182,16 +183,16 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { val stickyKeys by collectLastValue(viewModel.indicatorContent) setStickyKeysActive() - setStickyKeys(mapOf( - ALT to false, - META to false, - SHIFT to false)) + setStickyKeys(mapOf(ALT to false, META to false, SHIFT to false)) - assertThat(stickyKeys).isEqualTo(mapOf( - ALT to Locked(false), - META to Locked(false), - SHIFT to Locked(false), - )) + assertThat(stickyKeys) + .isEqualTo( + mapOf( + ALT to Locked(false), + META to Locked(false), + SHIFT to Locked(false), + ) + ) } } @@ -201,9 +202,7 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { val stickyKeys by collectLastValue(viewModel.indicatorContent) setStickyKeysActive() - setStickyKeys(mapOf( - ALT to false, - ALT to true)) + setStickyKeys(mapOf(ALT to false, ALT to true)) assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(true))) } @@ -215,17 +214,23 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { val stickyKeys by collectLastValue(viewModel.indicatorContent) setStickyKeysActive() - setStickyKeys(mapOf( - META to false, - SHIFT to false, // shift is sticky but not locked - CTRL to false)) + setStickyKeys( + mapOf( + META to false, + SHIFT to false, // shift is sticky but not locked + CTRL to false + ) + ) val previousShiftIndex = stickyKeys?.toList()?.indexOf(SHIFT to Locked(false)) - setStickyKeys(mapOf( - SHIFT to false, - SHIFT to true, // shift is now locked - META to false, - CTRL to false)) + setStickyKeys( + mapOf( + SHIFT to false, + SHIFT to true, // shift is now locked + META to false, + CTRL to false + ) + ) assertThat(stickyKeys?.toList()?.indexOf(SHIFT to Locked(true))) .isEqualTo(previousShiftIndex) } @@ -247,17 +252,27 @@ class StickyKeysIndicatorViewModelTest : SysuiTestCase() { StickyModifierState() { private fun isOn(key: ModifierKey) = keys.any { it.key == key && !it.value } + private fun isLocked(key: ModifierKey) = keys.any { it.key == key && it.value } override fun isAltGrModifierLocked() = isLocked(ALT_GR) + override fun isAltGrModifierOn() = isOn(ALT_GR) + override fun isAltModifierLocked() = isLocked(ALT) + override fun isAltModifierOn() = isOn(ALT) + override fun isCtrlModifierLocked() = isLocked(CTRL) + override fun isCtrlModifierOn() = isOn(CTRL) + override fun isMetaModifierLocked() = isLocked(META) + override fun isMetaModifierOn() = isOn(META) + override fun isShiftModifierLocked() = isLocked(SHIFT) + override fun isShiftModifierOn() = isOn(SHIFT) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt index 29cd9a270ed3..fa69fdd38b8e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt @@ -50,6 +50,8 @@ import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAfforda import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager +import com.android.systemui.kosmos.unconfinedTestDispatcher +import com.android.systemui.kosmos.unconfinedTestScope import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -64,12 +66,12 @@ import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings +import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before @@ -87,6 +89,11 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper(setAsMainLooper = true) class CustomizationProviderTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.unconfinedTestDispatcher + private val testScope = kosmos.unconfinedTestScope + private val fakeSettings = kosmos.unconfinedDispatcherFakeSettings + @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var userTracker: UserTracker @@ -104,9 +111,6 @@ class CustomizationProviderTest : SysuiTestCase() { private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository private lateinit var underTest: CustomizationProvider - private lateinit var testScope: TestScope - - private val kosmos = testKosmos() @Before fun setUp() { @@ -120,8 +124,6 @@ class CustomizationProviderTest : SysuiTestCase() { biometricSettingsRepository = FakeBiometricSettingsRepository() underTest = CustomizationProvider() - val testDispatcher = UnconfinedTestDispatcher() - testScope = TestScope(testDispatcher) val localUserSelectionManager = KeyguardQuickAffordanceLocalUserSelectionManager( context = context, @@ -170,7 +172,7 @@ class CustomizationProviderTest : SysuiTestCase() { KeyguardQuickAffordanceLegacySettingSyncer( scope = testScope.backgroundScope, backgroundDispatcher = testDispatcher, - secureSettings = FakeSettings(), + secureSettings = fakeSettings, selectionsManager = localUserSelectionManager, ), dumpManager = mock(), @@ -216,7 +218,7 @@ class CustomizationProviderTest : SysuiTestCase() { mainDispatcher = testDispatcher, backgroundHandler = backgroundHandler, ) - underTest.mainDispatcher = UnconfinedTestDispatcher() + underTest.mainDispatcher = testDispatcher underTest.attachInfoForTesting( context, @@ -319,6 +321,7 @@ class CustomizationProviderTest : SysuiTestCase() { ), ) ) + runCurrent() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt index af5187d03261..1e9db6466de6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt @@ -25,15 +25,14 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.ClockSizeSetting +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.shared.clocks.ClockRegistry -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import kotlin.test.Test -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.runner.RunWith @@ -44,12 +43,12 @@ import org.mockito.MockitoAnnotations @SmallTest class KeyguardClockRepositoryTest : SysuiTestCase() { - private lateinit var scheduler: TestCoroutineScheduler - private lateinit var dispatcher: CoroutineDispatcher - private lateinit var scope: TestScope + private val kosmos = testKosmos() + private val dispatcher = kosmos.testDispatcher + private val scope = kosmos.testScope + private val fakeSettings = kosmos.fakeSettings private lateinit var underTest: KeyguardClockRepository - private lateinit var fakeSettings: FakeSettings @Mock private lateinit var clockRegistry: ClockRegistry @Mock private lateinit var clockEventController: ClockEventController private val fakeFeatureFlagsClassic = FakeFeatureFlagsClassic() @@ -57,10 +56,6 @@ class KeyguardClockRepositoryTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) - fakeSettings = FakeSettings() - scheduler = TestCoroutineScheduler() - dispatcher = StandardTestDispatcher(scheduler) - scope = TestScope(dispatcher) underTest = KeyguardClockRepositoryImpl( fakeSettings, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt index 8b8a6cbf6148..5a597fe8e920 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt @@ -21,14 +21,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope import com.android.systemui.settings.FakeUserTracker -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import kotlin.test.Test -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.runner.RunWith @@ -38,23 +36,18 @@ import org.mockito.MockitoAnnotations @SmallTest class KeyguardSmartspaceRepositoryImplTest : SysuiTestCase() { - private lateinit var scheduler: TestCoroutineScheduler - private lateinit var dispatcher: CoroutineDispatcher - private lateinit var scope: TestScope + private val kosmos = testKosmos() + private val scope = kosmos.testScope + private val fakeSettings = kosmos.fakeSettings private lateinit var underTest: KeyguardSmartspaceRepository - private lateinit var fakeSettings: FakeSettings private lateinit var fakeUserTracker: FakeUserTracker @Before fun setup() { MockitoAnnotations.initMocks(this) - fakeSettings = FakeSettings() fakeUserTracker = FakeUserTracker() fakeSettings.userId = fakeUserTracker.userId - scheduler = TestCoroutineScheduler() - dispatcher = StandardTestDispatcher(scheduler) - scope = TestScope(dispatcher) underTest = KeyguardSmartspaceRepositoryImpl( context = context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index d13419eed281..1929cd172750 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -54,6 +54,8 @@ import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInterac import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -69,13 +71,11 @@ import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min import kotlinx.coroutines.flow.map -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -93,6 +93,11 @@ import platform.test.runner.parameterized.Parameters @RunWith(ParameterizedAndroidJunit4::class) class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val settings = kosmos.fakeSettings + @Mock private lateinit var expandable: Expandable @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper @Mock private lateinit var lockPatternUtils: LockPatternUtils @@ -108,7 +113,6 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC private lateinit var underTest: KeyguardBottomAreaViewModel - private lateinit var testScope: TestScope private lateinit var repository: FakeKeyguardRepository private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig @@ -116,8 +120,6 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC private lateinit var dockManager: DockManagerFake private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository - private val kosmos = testKosmos() - init { mSetFlagsRule.setFlagsParameterization(flags) } @@ -162,8 +164,6 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) - val testDispatcher = StandardTestDispatcher() - testScope = TestScope(testDispatcher) val localUserSelectionManager = KeyguardQuickAffordanceLocalUserSelectionManager( context = context, @@ -199,7 +199,7 @@ class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestC KeyguardQuickAffordanceLegacySettingSyncer( scope = testScope.backgroundScope, backgroundDispatcher = testDispatcher, - secureSettings = FakeSettings(), + secureSettings = settings, selectionsManager = localUserSelectionManager, ), configs = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index df1052370ff4..720f2e197aea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -72,7 +72,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth import kotlin.math.min import kotlin.test.assertEquals @@ -94,6 +94,10 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val settings = kosmos.fakeSettings + @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var expandable: Expandable @@ -151,11 +155,8 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { private lateinit var glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel - private val kosmos = testKosmos() - private lateinit var underTest: KeyguardQuickAffordancesCombinedViewModel - private val testScope = kosmos.testScope private lateinit var repository: FakeKeyguardRepository private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig @@ -244,7 +245,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { KeyguardQuickAffordanceLegacySettingSyncer( scope = testScope.backgroundScope, backgroundDispatcher = kosmos.testDispatcher, - secureSettings = FakeSettings(), + secureSettings = settings, selectionsManager = localUserSelectionManager, ), configs = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 9eccd9fd3cdf..f4c2b47d6653 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -84,7 +84,7 @@ import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -141,6 +141,11 @@ private fun <T> anyObject(): T { @RunWith(ParameterizedAndroidJunit4::class) @EnableSceneContainer class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val settings = kosmos.fakeSettings + @JvmField @Rule val mockito = MockitoJUnit.rule() @Mock lateinit var controller: MediaController @Mock lateinit var transportControls: MediaController.TransportControls @@ -193,9 +198,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { mSetFlagsRule.setFlagsParameterization(flags) } - private val kosmos = testKosmos() - private val testDispatcher = kosmos.testDispatcher - private val testScope = kosmos.testScope private val fakeFeatureFlags = kosmos.fakeFeatureFlagsClassic private val activityStarter = kosmos.activityStarter private val mediaControllerFactory = kosmos.fakeMediaControllerFactory @@ -203,7 +205,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { private val mediaFilterRepository = kosmos.mediaFilterRepository private val mediaDataFilter = kosmos.mediaDataFilter - private val settings = FakeSettings() private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) private val originalSmartspaceSetting = @@ -1835,10 +1836,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(userEntries).hasSize(1) val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! - assertThat(secondSemanticActions.playOrPause?.icon) - .isEqualTo(firstSemanticActions.playOrPause?.icon) - assertThat(secondSemanticActions.playOrPause?.background) - .isEqualTo(firstSemanticActions.playOrPause?.background) assertThat(secondSemanticActions.nextOrCustom?.icon) .isEqualTo(firstSemanticActions.nextOrCustom?.icon) assertThat(secondSemanticActions.prevOrCustom?.icon) @@ -1873,11 +1870,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(userEntries).hasSize(1) val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! - - assertThat(secondSemanticActions.playOrPause?.icon) - .isNotEqualTo(firstSemanticActions.playOrPause?.icon) - assertThat(secondSemanticActions.playOrPause?.background) - .isNotEqualTo(firstSemanticActions.playOrPause?.background) assertThat(secondSemanticActions.nextOrCustom?.icon) .isNotEqualTo(firstSemanticActions.nextOrCustom?.icon) assertThat(secondSemanticActions.prevOrCustom?.icon) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index 46c66e03fd9f..03667cfb8a3b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticati import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.unconfinedTestDispatcher import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -72,9 +73,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings -import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.unconfinedDispatcherFakeSettings import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Locale @@ -84,9 +84,7 @@ import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After @@ -120,7 +118,9 @@ private const val PLAYING_LOCAL = "playing local" @TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class MediaCarouselControllerTest : SysuiTestCase() { - val kosmos = testKosmos() + private val kosmos = testKosmos() + private val testDispatcher = kosmos.unconfinedTestDispatcher + private val secureSettings = kosmos.unconfinedDispatcherFakeSettings @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel> @Mock lateinit var mediaViewControllerFactory: Provider<MediaViewController> @@ -142,7 +142,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var mediaFlags: MediaFlags @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock lateinit var globalSettings: GlobalSettings - private lateinit var secureSettings: SecureSettings private val transitionRepository = kosmos.fakeKeyguardTransitionRepository @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> @Captor @@ -154,7 +153,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { private val clock = FakeSystemClock() private lateinit var bgExecutor: FakeExecutor - private lateinit var testDispatcher: TestDispatcher private lateinit var mediaCarouselController: MediaCarouselController private var originalResumeSetting = @@ -163,10 +161,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) - secureSettings = FakeSettings() context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK)) bgExecutor = FakeExecutor(clock) - testDispatcher = UnconfinedTestDispatcher() mediaCarouselController = MediaCarouselController( applicationScope = kosmos.applicationCoroutineScope, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt index 8435b1cb71dc..d2dcf4d38d0c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.tiles -import android.graphics.drawable.TestStubDrawable import android.os.Handler import android.platform.test.annotations.EnableFlags import android.service.quicksettings.Tile @@ -93,12 +92,7 @@ class ModesTileTest : SysuiTestCase() { ModesTileDataInteractor(context, kosmos.zenModeInteractor, testDispatcher) private val mapper = ModesTileMapper( - context.orCreateTestableResources - .apply { - addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable()) - addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable()) - } - .resources, + context.resources, context.theme, ) @@ -122,7 +116,7 @@ class ModesTileTest : SysuiTestCase() { QSTileConfigTestBuilder.build { uiConfig = QSTileUIConfig.Resource( - iconRes = R.drawable.qs_dnd_icon_off, + iconRes = ModesTile.ICON_RES_ID, labelRes = R.string.quick_settings_modes_label, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt new file mode 100644 index 000000000000..d8618fa71aab --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/CustomTraceStateTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 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.recordissue + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC +import com.android.traceur.PresetTraceConfigs +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +/** + * CustomTraceState is customized Traceur settings for power users. These settings determine what + * tracing is used during the Record Issue Quick Settings flow. This class tests that those features + * are persistently and accurately stored across sessions. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class CustomTraceStateTest : SysuiTestCase() { + + private lateinit var underTest: CustomTraceState + + @Before + fun setup() { + underTest = CustomTraceState(context.getSharedPreferences(TILE_SPEC, 0)) + MockitoAnnotations.initMocks(this) + } + + @Test + fun trace_config_is_stored_accurately() { + val expected = PresetTraceConfigs.getUiConfig() + + underTest.traceConfig = expected + + val actual = underTest.traceConfig + Truth.assertThat(actual.longTrace).isEqualTo(expected.longTrace) + Truth.assertThat(actual.maxLongTraceSizeMb).isEqualTo(expected.maxLongTraceSizeMb) + Truth.assertThat(actual.maxLongTraceDurationMinutes) + .isEqualTo(expected.maxLongTraceDurationMinutes) + Truth.assertThat(actual.apps).isEqualTo(expected.apps) + Truth.assertThat(actual.winscope).isEqualTo(expected.winscope) + Truth.assertThat(actual.attachToBugreport).isEqualTo(expected.attachToBugreport) + Truth.assertThat(actual.bufferSizeKb).isEqualTo(expected.bufferSizeKb) + Truth.assertThat(actual.tags).isEqualTo(expected.tags) + } + + @Test + fun trace_config_is_persistently_stored_between_instances() { + val expected = PresetTraceConfigs.getUiConfig() + + underTest.traceConfig = expected + + val actual = CustomTraceState(context.getSharedPreferences(TILE_SPEC, 0)).traceConfig + Truth.assertThat(actual.longTrace).isEqualTo(expected.longTrace) + Truth.assertThat(actual.maxLongTraceSizeMb).isEqualTo(expected.maxLongTraceSizeMb) + Truth.assertThat(actual.maxLongTraceDurationMinutes) + .isEqualTo(expected.maxLongTraceDurationMinutes) + Truth.assertThat(actual.apps).isEqualTo(expected.apps) + Truth.assertThat(actual.winscope).isEqualTo(expected.winscope) + Truth.assertThat(actual.attachToBugreport).isEqualTo(expected.attachToBugreport) + Truth.assertThat(actual.bufferSizeKb).isEqualTo(expected.bufferSizeKb) + Truth.assertThat(actual.tags).isEqualTo(expected.tags) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt index b31d21e7f8b2..15da77dfe33d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt @@ -2,8 +2,6 @@ package com.android.systemui.screenshot import android.graphics.drawable.Drawable import android.os.UserHandle -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -90,20 +88,6 @@ class MessageContainerControllerTest : SysuiTestCase() { } @Test - @DisableFlags(com.android.systemui.Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX) - fun testOnScreenshotTakenUserHandle_withWorkProfileFirstRun() { - whenever(workProfileMessageController.onScreenshotTaken(eq(userHandle))) - .thenReturn(workProfileData) - messageContainer.onScreenshotTaken(screenshotData) - - verify(workProfileMessageController) - .populateView(eq(workProfileFirstRunView), eq(workProfileData), any()) - assertEquals(View.VISIBLE, workProfileFirstRunView.visibility) - assertEquals(View.GONE, detectionNoticeView.visibility) - } - - @Test - @EnableFlags(com.android.systemui.Flags.FLAG_SCREENSHOT_PRIVATE_PROFILE_BEHAVIOR_FIX) fun testOnScreenshotTakenUserHandle_withProfileProfileFirstRun() = runTest { val profileData = ProfileMessageController.ProfileFirstRunData( diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt deleted file mode 100644 index 0847f01f12d3..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt +++ /dev/null @@ -1,184 +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.screenshot - -import android.content.ComponentName -import android.graphics.Bitmap -import android.graphics.ColorSpace -import android.graphics.Insets -import android.graphics.Rect -import android.hardware.HardwareBuffer -import android.os.UserHandle -import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER -import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER -import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN -import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE -import com.android.internal.util.ScreenshotRequest -import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.runBlocking -import org.junit.Assert -import org.junit.Test - -private const val USER_ID = 1 -private const val TASK_ID = 1 - -class RequestProcessorTest { - private val imageCapture = FakeImageCapture() - private val component = ComponentName("android.test", "android.test.Component") - private val bounds = Rect(25, 25, 75, 75) - - private val policy = FakeScreenshotPolicy() - - @Test - fun testFullScreenshot() = runBlocking { - // Indicate that the primary content belongs to a normal user - policy.setManagedProfile(USER_ID, false) - policy.setDisplayContentInfo( - policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID) - ) - - val request = - ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build() - val processor = RequestProcessor(imageCapture, policy) - - val processedData = processor.process(ScreenshotData.fromRequest(request)) - - // Request has topComponent added, but otherwise unchanged. - assertThat(processedData.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN) - assertThat(processedData.source).isEqualTo(SCREENSHOT_OTHER) - assertThat(processedData.topComponent).isEqualTo(component) - } - - @Test - fun testFullScreenshot_managedProfile() = runBlocking { - // Provide a fake task bitmap when asked - val bitmap = makeHardwareBitmap(100, 100) - imageCapture.image = bitmap - - // Indicate that the primary content belongs to a manged profile - policy.setManagedProfile(USER_ID, true) - policy.setDisplayContentInfo( - policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID) - ) - - val request = - ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build() - val processor = RequestProcessor(imageCapture, policy) - - val processedData = processor.process(ScreenshotData.fromRequest(request)) - - // Expect a task snapshot is taken, overriding the full screen mode - assertThat(processedData.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE) - assertThat(processedData.bitmap).isEqualTo(bitmap) - assertThat(processedData.screenBounds).isEqualTo(bounds) - assertThat(processedData.insets).isEqualTo(Insets.NONE) - assertThat(processedData.taskId).isEqualTo(TASK_ID) - assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID) - } - - @Test - fun testFullScreenshot_managedProfile_nullBitmap() { - // Provide a null task bitmap when asked - imageCapture.image = null - - // Indicate that the primary content belongs to a manged profile - policy.setManagedProfile(USER_ID, true) - policy.setDisplayContentInfo( - policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID) - ) - - val request = - ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build() - val processor = RequestProcessor(imageCapture, policy) - - Assert.assertThrows(IllegalStateException::class.java) { - runBlocking { processor.process(ScreenshotData.fromRequest(request)) } - } - } - - @Test - fun testProvidedImageScreenshot() = runBlocking { - val bounds = Rect(50, 50, 150, 150) - val processor = RequestProcessor(imageCapture, policy) - - policy.setManagedProfile(USER_ID, false) - - val bitmap = makeHardwareBitmap(100, 100) - - val request = - ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER) - .setTopComponent(component) - .setTaskId(TASK_ID) - .setUserId(USER_ID) - .setBitmap(bitmap) - .setBoundsOnScreen(bounds) - .setInsets(Insets.NONE) - .build() - - val screenshotData = ScreenshotData.fromRequest(request) - val processedData = processor.process(screenshotData) - - assertThat(processedData).isEqualTo(screenshotData) - } - - @Test - fun testProvidedImageScreenshot_managedProfile() = runBlocking { - val bounds = Rect(50, 50, 150, 150) - val processor = RequestProcessor(imageCapture, policy) - - // Indicate that the screenshot belongs to a manged profile - policy.setManagedProfile(USER_ID, true) - - val bitmap = makeHardwareBitmap(100, 100) - - val request = - ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER) - .setTopComponent(component) - .setTaskId(TASK_ID) - .setUserId(USER_ID) - .setBitmap(bitmap) - .setBoundsOnScreen(bounds) - .setInsets(Insets.NONE) - .build() - - val screenshotData = ScreenshotData.fromRequest(request) - val processedData = processor.process(screenshotData) - - // Work profile, but already a task snapshot, so no changes - assertThat(processedData).isEqualTo(screenshotData) - } - - private fun makeHardwareBitmap(width: Int, height: Int): Bitmap { - val buffer = - HardwareBuffer.create( - width, - height, - HardwareBuffer.RGBA_8888, - 1, - HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE - ) - return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!! - } - - private fun Bitmap.equalsHardwareBitmap(bitmap: Bitmap): Boolean { - return bitmap.hardwareBuffer == this.hardwareBuffer && bitmap.colorSpace == this.colorSpace - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index 774aa517672e..2e2ac3eb7183 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -33,9 +33,11 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import com.google.common.truth.TruthJUnit.assume +import java.util.concurrent.Executor import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -54,10 +56,7 @@ import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.concurrent.Executor - @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -71,27 +70,19 @@ class UserTrackerImplTest : SysuiTestCase() { fun isBackgroundUserTrackerEnabled(): Iterable<Boolean> = listOf(true, false) } - @Mock - private lateinit var context: Context + @Mock private lateinit var context: Context - @Mock - private lateinit var userManager: UserManager + @Mock private lateinit var userManager: UserManager - @Mock - private lateinit var iActivityManager: IActivityManager + @Mock private lateinit var iActivityManager: IActivityManager - @Mock - private lateinit var userSwitchingReply: IRemoteCallback + @Mock private lateinit var userSwitchingReply: IRemoteCallback - @Mock(stubOnly = true) - private lateinit var dumpManager: DumpManager + @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager - @Mock(stubOnly = true) - private lateinit var handler: Handler + @Mock(stubOnly = true) private lateinit var handler: Handler - @Parameterized.Parameter - @JvmField - var isBackgroundUserTrackerEnabled: Boolean = false + @Parameterized.Parameter @JvmField var isBackgroundUserTrackerEnabled: Boolean = false private val testScope = TestScope() private val testDispatcher = StandardTestDispatcher(testScope.testScheduler) @@ -104,365 +95,379 @@ class UserTrackerImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - `when`(context.userId).thenReturn(UserHandle.USER_SYSTEM) - `when`(context.user).thenReturn(UserHandle.SYSTEM) - `when`(context.createContextAsUser(any(), anyInt())).thenAnswer { invocation -> + whenever(context.userId).thenReturn(UserHandle.USER_SYSTEM) + whenever(context.user).thenReturn(UserHandle.SYSTEM) + whenever(context.createContextAsUser(any(), anyInt())).thenAnswer { invocation -> val user = invocation.getArgument<UserHandle>(0) - `when`(context.user).thenReturn(user) - `when`(context.userId).thenReturn(user.identifier) + whenever(context.user).thenReturn(user) + whenever(context.userId).thenReturn(user.identifier) context } - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> val info = UserInfo(invocation.getArgument<Int>(0), "", UserInfo.FLAG_FULL) listOf(info) } featureFlags.set(Flags.USER_TRACKER_BACKGROUND_CALLBACKS, isBackgroundUserTrackerEnabled) tracker = - UserTrackerImpl( - context, - { featureFlags }, - userManager, - iActivityManager, - dumpManager, - testScope.backgroundScope, - testDispatcher, - handler, - ) + UserTrackerImpl( + context, + { featureFlags }, + userManager, + iActivityManager, + dumpManager, + testScope.backgroundScope, + testDispatcher, + handler, + ) } - @Test - fun testNotInitialized() = testScope.runTest { - assertThat(tracker.initialized).isFalse() - } + @Test fun testNotInitialized() = testScope.runTest { assertThat(tracker.initialized).isFalse() } @Test(expected = IllegalStateException::class) - fun testGetUserIdBeforeInitThrowsException() = testScope.runTest { - tracker.userId - } + fun testGetUserIdBeforeInitThrowsException() = testScope.runTest { tracker.userId } @Test(expected = IllegalStateException::class) - fun testGetUserHandleBeforeInitThrowsException() = testScope.runTest { - tracker.userHandle - } + fun testGetUserHandleBeforeInitThrowsException() = testScope.runTest { tracker.userHandle } @Test(expected = IllegalStateException::class) - fun testGetUserContextBeforeInitThrowsException() = testScope.runTest { - tracker.userContext - } + fun testGetUserContextBeforeInitThrowsException() = testScope.runTest { tracker.userContext } @Test(expected = IllegalStateException::class) - fun testGetUserContentResolverBeforeInitThrowsException() = testScope.runTest { - tracker.userContentResolver - } + fun testGetUserContentResolverBeforeInitThrowsException() = + testScope.runTest { tracker.userContentResolver } @Test(expected = IllegalStateException::class) - fun testGetUserProfilesBeforeInitThrowsException() = testScope.runTest { - tracker.userProfiles - } + fun testGetUserProfilesBeforeInitThrowsException() = testScope.runTest { tracker.userProfiles } @Test - fun testInitialize() = testScope.runTest { - tracker.initialize(0) + fun testInitialize() = + testScope.runTest { + tracker.initialize(0) - assertThat(tracker.initialized).isTrue() - } + assertThat(tracker.initialized).isTrue() + } @Test - fun testReceiverRegisteredOnInitialize() = testScope.runTest { - tracker.initialize(0) + fun testReceiverRegisteredOnInitialize() = + testScope.runTest { + tracker.initialize(0) - val captor = ArgumentCaptor.forClass(IntentFilter::class.java) + val captor = ArgumentCaptor.forClass(IntentFilter::class.java) - verify(context) + verify(context) .registerReceiverForAllUsers(eq(tracker), capture(captor), isNull(), eq(handler)) - with(captor.value) { - assertThat(countActions()).isEqualTo(11) - assertThat(hasAction(Intent.ACTION_LOCALE_CHANGED)).isTrue() - assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_ADDED)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_REMOVED)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_AVAILABLE)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)).isTrue() + with(captor.value) { + assertThat(countActions()).isEqualTo(11) + assertThat(hasAction(Intent.ACTION_LOCALE_CHANGED)).isTrue() + assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_ADDED)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_REMOVED)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_AVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)).isTrue() + } } - } @Test - fun testInitialValuesSet() = testScope.runTest { - val testID = 4 - tracker.initialize(testID) - - verify(userManager).getProfiles(testID) + fun testInitialValuesSet() = + testScope.runTest { + val testID = 4 + tracker.initialize(testID) - assertThat(tracker.userId).isEqualTo(testID) - assertThat(tracker.userHandle).isEqualTo(UserHandle.of(testID)) - assertThat(tracker.userContext.userId).isEqualTo(testID) - assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(testID)) - assertThat(tracker.userProfiles).hasSize(1) + verify(userManager).getProfiles(testID) - val info = tracker.userProfiles[0] - assertThat(info.id).isEqualTo(testID) - } + assertThat(tracker.userId).isEqualTo(testID) + assertThat(tracker.userHandle).isEqualTo(UserHandle.of(testID)) + assertThat(tracker.userContext.userId).isEqualTo(testID) + assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(testID)) + assertThat(tracker.userProfiles).hasSize(1) - @Test - fun testUserSwitch() = testScope.runTest { - tracker.initialize(0) - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) - captor.value.onUserSwitching(newID, userSwitchingReply) - runCurrent() - verify(userSwitchingReply).sendResult(any()) - - verify(userManager).getProfiles(newID) - - assertThat(tracker.userId).isEqualTo(newID) - assertThat(tracker.userHandle).isEqualTo(UserHandle.of(newID)) - assertThat(tracker.userContext.userId).isEqualTo(newID) - assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(newID)) - assertThat(tracker.userProfiles).hasSize(1) - - val info = tracker.userProfiles[0] - assertThat(info.id).isEqualTo(newID) - } + val info = tracker.userProfiles[0] + assertThat(info.id).isEqualTo(testID) + } @Test - fun testManagedProfileAvailable() = testScope.runTest { - tracker.initialize(0) - val profileID = tracker.userId + 10 - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) + fun testUserSwitch() = + testScope.runTest { + tracker.initialize(0) + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) + captor.value.onUserSwitching(newID, userSwitchingReply) + runCurrent() + verify(userSwitchingReply).sendResult(any()) + + verify(userManager).getProfiles(newID) + + assertThat(tracker.userId).isEqualTo(newID) + assertThat(tracker.userHandle).isEqualTo(UserHandle.of(newID)) + assertThat(tracker.userContext.userId).isEqualTo(newID) + assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(newID)) + assertThat(tracker.userProfiles).hasSize(1) + + val info = tracker.userProfiles[0] + assertThat(info.id).isEqualTo(newID) } - val intent = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent) - - assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID) - } - @Test - fun testManagedProfileUnavailable() = testScope.runTest { - tracker.initialize(0) - val profileID = tracker.userId + 10 - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE or UserInfo.FLAG_QUIET_MODE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) + fun testManagedProfileAvailable() = + testScope.runTest { + tracker.initialize(0) + val profileID = tracker.userId + 10 + + whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = + UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) + } + + val intent = + Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent) + + assertThat(tracker.userProfiles.map { it.id }) + .containsExactly(tracker.userId, profileID) } - val intent = Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent) - - assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID) - } - @Test - fun testManagedProfileStartedAndRemoved() = testScope.runTest { - tracker.initialize(0) - val profileID = tracker.userId + 10 - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) + fun testManagedProfileUnavailable() = + testScope.runTest { + tracker.initialize(0) + val profileID = tracker.userId + 10 + + whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = + UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE or UserInfo.FLAG_QUIET_MODE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) + } + + val intent = + Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent) + + assertThat(tracker.userProfiles.map { it.id }) + .containsExactly(tracker.userId, profileID) } - // Managed profile started - val intent = Intent(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent) - - assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID) - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - listOf(UserInfo(invocation.getArgument(0), "", UserInfo.FLAG_FULL)) + @Test + fun testManagedProfileStartedAndRemoved() = + testScope.runTest { + tracker.initialize(0) + val profileID = tracker.userId + 10 + + whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = + UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) + } + + // Managed profile started + val intent = + Intent(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent) + + assertThat(tracker.userProfiles.map { it.id }) + .containsExactly(tracker.userId, profileID) + + whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + listOf(UserInfo(invocation.getArgument(0), "", UserInfo.FLAG_FULL)) + } + + val intent2 = + Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent2) + + assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId) } - val intent2 = Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent2) - - assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId) - } - @Test - fun testCallbackNotCalledOnAdd() = testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() + fun testCallbackNotCalledOnAdd() = + testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() - tracker.addCallback(callback, executor) + tracker.addCallback(callback, executor) - assertThat(callback.calledOnProfilesChanged).isEqualTo(0) - assertThat(callback.calledOnUserChanged).isEqualTo(0) - } + assertThat(callback.calledOnProfilesChanged).isEqualTo(0) + assertThat(callback.calledOnUserChanged).isEqualTo(0) + } @Test - fun testCallbackCalledOnUserChanging() = testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) - captor.value.onUserSwitching(newID, userSwitchingReply) - runCurrent() - - verify(userSwitchingReply).sendResult(any()) - assertThat(callback.calledOnUserChanging).isEqualTo(1) - assertThat(callback.lastUser).isEqualTo(newID) - assertThat(callback.lastUserContext?.userId).isEqualTo(newID) - } + fun testCallbackCalledOnUserChanging() = + testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() + tracker.addCallback(callback, executor) + + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) + captor.value.onUserSwitching(newID, userSwitchingReply) + runCurrent() + + verify(userSwitchingReply).sendResult(any()) + assertThat(callback.calledOnUserChanging).isEqualTo(1) + assertThat(callback.lastUser).isEqualTo(newID) + assertThat(callback.lastUserContext?.userId).isEqualTo(newID) + } @Test - fun testAsyncCallbackWaitsUserToChange() = testScope.runTest { - // Skip this test for CountDownLatch variation. The problem is that there would be a - // deadlock if the callbacks processing runs on the same thread as the callback (which - // is blocked by the latch). Before the change it works because the callbacks are - // processed on a binder thread which is always distinct. - // This is the issue that this feature addresses. - assume().that(isBackgroundUserTrackerEnabled).isTrue() - - tracker.initialize(0) - val callback = TestCallback() - val callbackExecutor = FakeExecutor(FakeSystemClock()) - tracker.addCallback(callback, callbackExecutor) - - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onUserSwitching(newID, userSwitchingReply) - - assertThat(callback.calledOnUserChanging).isEqualTo(0) - verify(userSwitchingReply, never()).sendResult(any()) - - FakeExecutor.exhaustExecutors(callbackExecutor) - runCurrent() - FakeExecutor.exhaustExecutors(callbackExecutor) - runCurrent() - - assertThat(callback.calledOnUserChanging).isEqualTo(1) - verify(userSwitchingReply).sendResult(any()) - } + fun testAsyncCallbackWaitsUserToChange() = + testScope.runTest { + // Skip this test for CountDownLatch variation. The problem is that there would be a + // deadlock if the callbacks processing runs on the same thread as the callback (which + // is blocked by the latch). Before the change it works because the callbacks are + // processed on a binder thread which is always distinct. + // This is the issue that this feature addresses. + assume().that(isBackgroundUserTrackerEnabled).isTrue() + + tracker.initialize(0) + val callback = TestCallback() + val callbackExecutor = FakeExecutor(FakeSystemClock()) + tracker.addCallback(callback, callbackExecutor) + + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onUserSwitching(newID, userSwitchingReply) + + assertThat(callback.calledOnUserChanging).isEqualTo(0) + verify(userSwitchingReply, never()).sendResult(any()) + + FakeExecutor.exhaustExecutors(callbackExecutor) + runCurrent() + FakeExecutor.exhaustExecutors(callbackExecutor) + runCurrent() + + assertThat(callback.calledOnUserChanging).isEqualTo(1) + verify(userSwitchingReply).sendResult(any()) + } @Test - fun testCallbackCalledOnUserChanged() = testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) - captor.value.onUserSwitchComplete(newID) - runCurrent() - - assertThat(callback.calledOnUserChanged).isEqualTo(1) - assertThat(callback.lastUser).isEqualTo(newID) - assertThat(callback.lastUserContext?.userId).isEqualTo(newID) - assertThat(callback.calledOnProfilesChanged).isEqualTo(1) - assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(newID) - } + fun testCallbackCalledOnUserChanged() = + testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() + tracker.addCallback(callback, executor) + + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) + captor.value.onUserSwitchComplete(newID) + runCurrent() + + assertThat(callback.calledOnUserChanged).isEqualTo(1) + assertThat(callback.lastUser).isEqualTo(newID) + assertThat(callback.lastUserContext?.userId).isEqualTo(newID) + assertThat(callback.calledOnProfilesChanged).isEqualTo(1) + assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(newID) + } @Test - fun testCallbackCalledOnUserInfoChanged() = testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - val profileID = tracker.userId + 10 - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) + fun testCallbackCalledOnUserInfoChanged() = + testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() + tracker.addCallback(callback, executor) + val profileID = tracker.userId + 10 + + whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = + UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) + } + + val intent = + Intent(Intent.ACTION_USER_INFO_CHANGED) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + + tracker.onReceive(context, intent) + + assertThat(callback.calledOnUserChanged).isEqualTo(0) + assertThat(callback.calledOnProfilesChanged).isEqualTo(1) + assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID) } - val intent = Intent(Intent.ACTION_USER_INFO_CHANGED) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - - tracker.onReceive(context, intent) - - assertThat(callback.calledOnUserChanged).isEqualTo(0) - assertThat(callback.calledOnProfilesChanged).isEqualTo(1) - assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID) - } - @Test - fun testCallbackRemoved() = testScope.runTest { - tracker.initialize(0) - val newID = 5 - val profileID = newID + 10 - - val callback = TestCallback() - tracker.addCallback(callback, executor) - tracker.removeCallback(callback) - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onUserSwitching(newID, userSwitchingReply) - runCurrent() - verify(userSwitchingReply).sendResult(any()) - captor.value.onUserSwitchComplete(newID) - - val intentProfiles = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - - tracker.onReceive(context, intentProfiles) - - assertThat(callback.calledOnUserChanging).isEqualTo(0) - assertThat(callback.calledOnUserChanged).isEqualTo(0) - assertThat(callback.calledOnProfilesChanged).isEqualTo(0) - } + fun testCallbackRemoved() = + testScope.runTest { + tracker.initialize(0) + val newID = 5 + val profileID = newID + 10 + + val callback = TestCallback() + tracker.addCallback(callback, executor) + tracker.removeCallback(callback) + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onUserSwitching(newID, userSwitchingReply) + runCurrent() + verify(userSwitchingReply).sendResult(any()) + captor.value.onUserSwitchComplete(newID) + + val intentProfiles = + Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + + tracker.onReceive(context, intentProfiles) + + assertThat(callback.calledOnUserChanging).isEqualTo(0) + assertThat(callback.calledOnUserChanged).isEqualTo(0) + assertThat(callback.calledOnProfilesChanged).isEqualTo(0) + } private class TestCallback : UserTracker.Callback { var calledOnUserChanging = 0 diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt index fadb1d7c91a1..b65a90200837 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt @@ -156,6 +156,25 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { } @Test + fun qsFullscreen_falseWhenIdleSplitShadeQs() = + testScope.runTest { + val actual by collectLastValue(underTest.isQsFullscreen) + + // WHEN split shade is enabled and Idle on QuickSettings scene + shadeTestUtil.setSplitShade(true) + keyguardRepository.setStatusBarState(StatusBarState.SHADE) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.QuickSettings) + ) + sceneInteractor.setTransitionState(transitionState) + runCurrent() + + // THEN QS is not fullscreen + Truth.assertThat(actual).isFalse() + } + + @Test fun qsFullscreen_trueWhenIdleQS() = testScope.runTest { val actual by collectLastValue(underTest.isQsFullscreen) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt index 8576893216d0..118dea6376bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.commandline.commandRegistry @@ -103,6 +104,22 @@ class DemoRonChipViewModelTest : SysuiTestCase() { @Test @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) + fun chip_supportsColor() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + commandRegistry.onShellCommand( + pw, + arrayOf("demo-ron", "-p", "com.android.systemui", "-c", "#434343") + ) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat((latest as OngoingActivityChipModel.Shown).colors) + .isInstanceOf(ColorsModel.Custom::class.java) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) fun chip_hasHideArg_hidden() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ValueParserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ValueParserTest.kt index 8cf7473345a7..1efb3f74b04f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ValueParserTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/commandline/ValueParserTest.kt @@ -40,6 +40,15 @@ class ValueParserTest : SysuiTestCase() { } @Test + fun parseColor() { + assertThat(Type.Color.parseValue("#434343").isSuccess).isTrue() + assertThat(Type.Color.parseValue("#aa123456").isSuccess).isTrue() + assertThat(Type.Color.parseValue("red").isSuccess).isTrue() + + assertThat(Type.Color.parseValue("not a color").isFailure).isTrue() + } + + @Test fun mapToComplexType() { val parseSquare = Type.Int.map { Rect(it, it, it, it) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt index 2f810276d7b2..c7919df7e0d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImplTest.kt @@ -20,6 +20,7 @@ import android.content.pm.UserInfo import android.os.UserManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor @@ -35,6 +36,7 @@ import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import org.mockito.kotlin.never @SmallTest @RunWith(AndroidJUnit4::class) @@ -46,12 +48,20 @@ class ManagedProfileControllerImplTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var userManager: UserManager + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Before fun setup() { MockitoAnnotations.initMocks(this) - controller = ManagedProfileControllerImpl(context, mainExecutor, userTracker, userManager) + controller = + ManagedProfileControllerImpl( + context, + mainExecutor, + userTracker, + userManager, + keyguardUpdateMonitor + ) } @Test @@ -107,6 +117,24 @@ class ManagedProfileControllerImplTest : SysuiTestCase() { captor.value.onProfilesChanged(userManager.getEnabledProfiles(1)) } + @Test + fun hasWorkingProfile_setWorkModeEnabled_callsAwakenFromDream() { + `when`(userTracker.userId).thenReturn(1) + setupWorkingProfile(1) + `when`(userManager.requestQuietModeEnabled(any(), any())).thenReturn(false) + controller.hasActiveProfile() + + controller.isWorkModeEnabled = true + + verify(keyguardUpdateMonitor).awakenFromDream() + } + + @Test + fun noWorkingProfile_setWorkModeEnabled_NoAwakenFromDreamCall() { + controller.isWorkModeEnabled = true + verify(keyguardUpdateMonitor, never()).awakenFromDream() + } + private fun setupWorkingProfile(userId: Int) { `when`(userManager.getEnabledProfiles(userId)) .thenReturn( diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 37a73cf62929..c235954dffa4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -24,21 +24,21 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.unconfinedTestDispatcher +import com.android.systemui.kosmos.unconfinedTestScope import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.testKosmos import com.android.systemui.user.data.model.SelectedUserModel import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.model.UserSwitcherSettingsModel -import com.android.systemui.util.settings.FakeGlobalSettings +import com.android.systemui.util.settings.unconfinedDispatcherFakeGlobalSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -52,143 +52,153 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class UserRepositoryImplTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testDispatcher = kosmos.unconfinedTestDispatcher + private val testScope = kosmos.unconfinedTestScope + private val globalSettings = kosmos.unconfinedDispatcherFakeGlobalSettings + @Mock private lateinit var manager: UserManager private lateinit var underTest: UserRepositoryImpl - private lateinit var globalSettings: FakeGlobalSettings private lateinit var tracker: FakeUserTracker @Before fun setUp() { MockitoAnnotations.initMocks(this) - - globalSettings = FakeGlobalSettings() tracker = FakeUserTracker() } @Test - fun userSwitcherSettings() = runSelfCancelingTest { - setUpGlobalSettings( - isSimpleUserSwitcher = true, - isAddUsersFromLockscreen = true, - isUserSwitcherEnabled = true, - ) - underTest = create(this) - - var value: UserSwitcherSettingsModel? = null - underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) - - assertUserSwitcherSettings( - model = value, - expectedSimpleUserSwitcher = true, - expectedAddUsersFromLockscreen = true, - expectedUserSwitcherEnabled = true, - ) - - setUpGlobalSettings( - isSimpleUserSwitcher = false, - isAddUsersFromLockscreen = true, - isUserSwitcherEnabled = true, - ) - assertUserSwitcherSettings( - model = value, - expectedSimpleUserSwitcher = false, - expectedAddUsersFromLockscreen = true, - expectedUserSwitcherEnabled = true, - ) - } - - @Test - fun userSwitcherSettings_isUserSwitcherEnabled_notInitialized() = runSelfCancelingTest { - underTest = create(this) - - var value: UserSwitcherSettingsModel? = null - underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) - - assertUserSwitcherSettings( - model = value, - expectedSimpleUserSwitcher = false, - expectedAddUsersFromLockscreen = false, - expectedUserSwitcherEnabled = - context.resources.getBoolean( - com.android.internal.R.bool.config_showUserSwitcherByDefault - ), - ) - } - - @Test - fun refreshUsers() = runSelfCancelingTest { - val mainUserId = 10 - val mainUser = mock(UserHandle::class.java) - whenever(manager.mainUser).thenReturn(mainUser) - whenever(mainUser.identifier).thenReturn(mainUserId) - - underTest = create(this) - val initialExpectedValue = - setUpUsers( - count = 3, - selectedIndex = 0, + fun userSwitcherSettings() = + testScope.runTest { + setUpGlobalSettings( + isSimpleUserSwitcher = true, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, ) - var userInfos: List<UserInfo>? = null - var selectedUserInfo: UserInfo? = null - underTest.userInfos.onEach { userInfos = it }.launchIn(this) - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) - - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(initialExpectedValue) - assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) - - val secondExpectedValue = - setUpUsers( - count = 4, - selectedIndex = 1, + underTest = create(testScope.backgroundScope) + var value: UserSwitcherSettingsModel? = null + val job = underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = true, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, ) - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(secondExpectedValue) - assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) - val selectedNonGuestUserId = selectedUserInfo?.id - val thirdExpectedValue = - setUpUsers( - count = 2, - isLastGuestUser = true, - selectedIndex = 1, + setUpGlobalSettings( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, ) - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(thirdExpectedValue) - assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) - assertThat(selectedUserInfo?.isGuest).isTrue() - assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) - assertThat(underTest.mainUserId).isEqualTo(mainUserId) - } + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + job.cancel() + } @Test - fun refreshUsers_sortsByCreationTime_guestUserLast() = runSelfCancelingTest { - underTest = create(this) - val unsortedUsers = - setUpUsers( - count = 3, - selectedIndex = 0, - isLastGuestUser = true, + fun userSwitcherSettings_isUserSwitcherEnabled_notInitialized() = + testScope.runTest { + underTest = create(testScope.backgroundScope) + + var value: UserSwitcherSettingsModel? = null + val job = underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = false, + expectedUserSwitcherEnabled = + context.resources.getBoolean( + com.android.internal.R.bool.config_showUserSwitcherByDefault + ), ) - unsortedUsers[0].creationTime = 999 - unsortedUsers[1].creationTime = 900 - unsortedUsers[2].creationTime = 950 - val expectedUsers = - listOf( - unsortedUsers[1], - unsortedUsers[0], - unsortedUsers[2], // last because this is the guest - ) - var userInfos: List<UserInfo>? = null - underTest.userInfos.onEach { userInfos = it }.launchIn(this) + job.cancel() + } - underTest.refreshUsers() - assertThat(userInfos).isEqualTo(expectedUsers) - } + @Test + fun refreshUsers() = + testScope.runTest { + val mainUserId = 10 + val mainUser = mock(UserHandle::class.java) + whenever(manager.mainUser).thenReturn(mainUser) + whenever(mainUser.identifier).thenReturn(mainUserId) + + underTest = create(testScope.backgroundScope) + val initialExpectedValue = + setUpUsers( + count = 3, + selectedIndex = 0, + ) + var userInfos: List<UserInfo>? = null + var selectedUserInfo: UserInfo? = null + val job1 = underTest.userInfos.onEach { userInfos = it }.launchIn(this) + val job2 = underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(initialExpectedValue) + assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val secondExpectedValue = + setUpUsers( + count = 4, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(secondExpectedValue) + assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val selectedNonGuestUserId = selectedUserInfo?.id + val thirdExpectedValue = + setUpUsers( + count = 2, + isLastGuestUser = true, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(thirdExpectedValue) + assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) + assertThat(selectedUserInfo?.isGuest).isTrue() + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) + assertThat(underTest.mainUserId).isEqualTo(mainUserId) + job1.cancel() + job2.cancel() + } + + @Test + fun refreshUsers_sortsByCreationTime_guestUserLast() = + testScope.runTest { + underTest = create(testScope.backgroundScope) + val unsortedUsers = + setUpUsers( + count = 3, + selectedIndex = 0, + isLastGuestUser = true, + ) + unsortedUsers[0].creationTime = 999 + unsortedUsers[1].creationTime = 900 + unsortedUsers[2].creationTime = 950 + val expectedUsers = + listOf( + unsortedUsers[1], + unsortedUsers[0], + unsortedUsers[2], // last because this is the guest + ) + var userInfos: List<UserInfo>? = null + val job = underTest.userInfos.onEach { userInfos = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(expectedUsers) + job.cancel() + } private fun setUpUsers( count: Int, @@ -206,58 +216,68 @@ class UserRepositoryImplTest : SysuiTestCase() { tracker.set(userInfos, selectedIndex) return userInfos } + @Test - fun userTrackerCallback_updatesSelectedUserInfo() = runSelfCancelingTest { - underTest = create(this) - var selectedUserInfo: UserInfo? = null - underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) - setUpUsers( - count = 2, - selectedIndex = 0, - ) - tracker.onProfileChanged() - assertThat(selectedUserInfo?.id).isEqualTo(0) - setUpUsers( - count = 2, - selectedIndex = 1, - ) - tracker.onProfileChanged() - assertThat(selectedUserInfo?.id).isEqualTo(1) - } + fun userTrackerCallback_updatesSelectedUserInfo() = + testScope.runTest { + underTest = create(testScope.backgroundScope) + var selectedUserInfo: UserInfo? = null + val job = underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + setUpUsers( + count = 2, + selectedIndex = 0, + ) + tracker.onProfileChanged() + assertThat(selectedUserInfo?.id).isEqualTo(0) + setUpUsers( + count = 2, + selectedIndex = 1, + ) + tracker.onProfileChanged() + assertThat(selectedUserInfo?.id).isEqualTo(1) + job.cancel() + } @Test - fun userTrackerCallback_updatesSelectionStatus() = runSelfCancelingTest { - underTest = create(this) - var selectedUser: SelectedUserModel? = null - underTest.selectedUser.onEach { selectedUser = it }.launchIn(this) - setUpUsers(count = 2, selectedIndex = 1) + fun userTrackerCallback_updatesSelectionStatus() = + testScope.runTest { + underTest = create(testScope.backgroundScope) + var selectedUser: SelectedUserModel? = null + val job = underTest.selectedUser.onEach { selectedUser = it }.launchIn(this) - // WHEN the user is changing - tracker.onUserChanging(userId = 1) + setUpUsers(count = 2, selectedIndex = 1) - // THEN the selection status is IN_PROGRESS - assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) + // WHEN the user is changing + tracker.onUserChanging(userId = 1) - // WHEN the user has finished changing - tracker.onUserChanged(userId = 1) + // THEN the selection status is IN_PROGRESS + assertThat(selectedUser!!.selectionStatus) + .isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) - // THEN the selection status is COMPLETE - assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE) + // WHEN the user has finished changing + tracker.onUserChanged(userId = 1) - tracker.onProfileChanged() - assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE) + // THEN the selection status is COMPLETE + assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE) - setUpUsers(count = 2, selectedIndex = 0) + tracker.onProfileChanged() + assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_COMPLETE) - tracker.onUserChanging(userId = 0) - assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) + setUpUsers(count = 2, selectedIndex = 0) - // WHEN a profile change occurs while a user is changing - tracker.onProfileChanged() + tracker.onUserChanging(userId = 0) + assertThat(selectedUser!!.selectionStatus) + .isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) - // THEN the selection status remains as IN_PROGRESS - assertThat(selectedUser!!.selectionStatus).isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) - } + // WHEN a profile change occurs while a user is changing + tracker.onProfileChanged() + + // THEN the selection status remains as IN_PROGRESS + assertThat(selectedUser!!.selectionStatus) + .isEqualTo(SelectionStatus.SELECTION_IN_PROGRESS) + job.cancel() + } private fun createUserInfo( id: Int, @@ -308,26 +328,13 @@ class UserRepositoryImplTest : SysuiTestCase() { assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) } - /** - * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which - * is then automatically canceled and cleaned-up. - */ - private fun runSelfCancelingTest( - block: suspend CoroutineScope.() -> Unit, - ) = - runBlocking(Dispatchers.Main.immediate) { - val scope = CoroutineScope(coroutineContext + Job()) - block(scope) - scope.cancel() - } - - private fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + private fun create(scope: CoroutineScope): UserRepositoryImpl { return UserRepositoryImpl( appContext = context, manager = manager, applicationScope = scope, - mainDispatcher = IMMEDIATE, - backgroundDispatcher = IMMEDIATE, + mainDispatcher = testDispatcher, + backgroundDispatcher = testDispatcher, globalSettings = globalSettings, tracker = tracker, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt index 88b2630bd78f..a0cfab4d2160 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt @@ -23,12 +23,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -40,9 +41,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class UserAwareSecureSettingsRepositoryTest : SysuiTestCase() { - private val dispatcher = StandardTestDispatcher() - private val testScope = TestScope(dispatcher) - private val secureSettings = FakeSettings() + private val kosmos = testKosmos() + private val dispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val secureSettings = kosmos.fakeSettings private val userRepository = Kosmos().fakeUserRepository private lateinit var repository: UserAwareSecureSettingsRepository diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 6e3936576bb4..3e7980da87e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -184,11 +184,11 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerImpl; -import com.android.wm.shell.common.bubbles.BubbleBarLocation; -import com.android.wm.shell.common.bubbles.BubbleBarUpdate; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/shared/flag/FakeComposeBouncerFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/shared/flag/FakeComposeBouncerFlags.kt deleted file mode 100644 index 7482c0feb56a..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/shared/flag/FakeComposeBouncerFlags.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2024 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.bouncer.shared.flag - -import com.android.systemui.scene.shared.flag.SceneContainerFlag - -class FakeComposeBouncerFlags(var composeBouncerEnabled: Boolean = false) : ComposeBouncerFlags { - override fun isComposeBouncerOrSceneContainerEnabled(): Boolean { - return SceneContainerFlag.isEnabled || composeBouncerEnabled - } - - @Deprecated( - "Avoid using this, this is meant to be used only by the glue code " + - "that includes compose bouncer in legacy keyguard.", - replaceWith = ReplaceWith("isComposeBouncerOrSceneContainerEnabled()") - ) - override fun isOnlyComposeBouncerEnabled(): Boolean = composeBouncerEnabled -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt index e8612d084b14..5c5969d359c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt @@ -22,7 +22,6 @@ import android.content.applicationContext import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor -import com.android.systemui.bouncer.shared.flag.composeBouncerFlags import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryBiometricsAllowedInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor @@ -45,7 +44,6 @@ val Kosmos.bouncerMessageViewModel by Fixture { faceAuthInteractor = deviceEntryFaceAuthInteractor, deviceUnlockedInteractor = deviceUnlockedInteractor, deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor, - flags = composeBouncerFlags, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index e405d17166b9..171be97bf964 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -25,7 +25,6 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor -import com.android.systemui.bouncer.shared.flag.composeBouncerFlags import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -55,7 +54,6 @@ val Kosmos.bouncerSceneContentViewModel by Fixture { authenticationInteractor = authenticationInteractor, devicePolicyManager = devicePolicyManager, bouncerMessageViewModelFactory = bouncerMessageViewModelFactory, - flags = composeBouncerFlags, userSwitcher = userSwitcherViewModel, actionButtonInteractor = bouncerActionButtonInteractor, pinViewModelFactory = pinBouncerViewModelFactory, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index 4ad046cc095e..629fda6b610c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -36,6 +36,7 @@ import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.settings.userTracker +import com.android.systemui.statusbar.phone.fakeManagedProfileController import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock @@ -61,6 +62,7 @@ val Kosmos.communalInteractor by Fixture { sceneInteractor = sceneInteractor, logBuffer = logcatLogBuffer("CommunalInteractor"), tableLogBuffer = mock(), + managedProfileController = fakeManagedProfileController ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index caa6e99d58cb..13116e7fd46f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.deviceentry.data.repository.deviceEntryRepository +import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -34,5 +35,6 @@ val Kosmos.deviceEntryInteractor by sceneInteractor = sceneInteractor, deviceUnlockedInteractor = deviceUnlockedInteractor, alternateBouncerInteractor = alternateBouncerInteractor, + dismissCallbackRegistry = dismissCallbackRegistry, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index b9443bcaf650..7cf4083e8407 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController @@ -39,6 +40,7 @@ val Kosmos.keyguardRootViewModel by Fixture { communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, notificationsKeyguardInteractor = notificationsKeyguardInteractor, + notificationShadeWindowModel = notificationShadeWindowModel, alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel, alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel, alternateBouncerToLockscreenTransitionViewModel = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index b34681ac0bdc..f8df7074142d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -5,9 +5,12 @@ import com.android.systemui.kosmos.Kosmos.Fixture import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() } +var Kosmos.unconfinedTestDispatcher by Fixture { UnconfinedTestDispatcher() } var Kosmos.testScope by Fixture { TestScope(testDispatcher) } +var Kosmos.unconfinedTestScope by Fixture { TestScope(unconfinedTestDispatcher) } var Kosmos.applicationCoroutineScope by Fixture { testScope.backgroundScope } var Kosmos.testCase: SysuiTestCase by Fixture() var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 953363d010fe..851a378f3165 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -36,6 +36,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.globalactions.domain.interactor.globalActionsInteractor +import com.android.systemui.haptics.msdl.msdlPlayer import com.android.systemui.haptics.qs.qsLongPressEffect import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -154,4 +155,5 @@ class KosmosJavaAdapter() { val scrimController by lazy { kosmos.scrimController } val scrimStartable by lazy { kosmos.scrimStartable } val sceneContainerOcclusionInteractor by lazy { kosmos.sceneContainerOcclusionInteractor } + val msdlPlayer by lazy { kosmos.msdlPlayer } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelKosmos.kt index 60d97d1b1437..8fc40e492bdc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlagsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelKosmos.kt @@ -14,9 +14,12 @@ * limitations under the License. */ -package com.android.systemui.bouncer.shared.flag +package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture -var Kosmos.fakeComposeBouncerFlags by Kosmos.Fixture { FakeComposeBouncerFlags() } -val Kosmos.composeBouncerFlags by Kosmos.Fixture<ComposeBouncerFlags> { fakeComposeBouncerFlags } +val Kosmos.quickSettingsShadeOverlayActionsViewModel: + QuickSettingsShadeOverlayActionsViewModel by Fixture { + QuickSettingsShadeOverlayActionsViewModel() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt index 00f1526f6cd4..ff8b478b368b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt @@ -14,25 +14,17 @@ * limitations under the License. */ -package com.android.systemui.shade.ui.viewmodel +package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory -val Kosmos.overlayShadeViewModel: OverlayShadeViewModel by +val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by Kosmos.Fixture { - OverlayShadeViewModel( + QuickSettingsShadeOverlayContentViewModel( sceneInteractor = sceneInteractor, - shadeInteractor = shadeInteractor, + shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, + quickSettingsContainerViewModel = quickSettingsContainerViewModel, ) } - -val Kosmos.overlayShadeViewModelFactory: OverlayShadeViewModel.Factory by - Kosmos.Fixture { - object : OverlayShadeViewModel.Factory { - override fun create(): OverlayShadeViewModel { - return overlayShadeViewModel - } - } - } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt index 5d70ed6a634c..128a7fcbab25 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt @@ -17,12 +17,10 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos -import com.android.systemui.shade.domain.interactor.shadeInteractor val Kosmos.quickSettingsShadeSceneActionsViewModel: QuickSettingsShadeSceneActionsViewModel by Kosmos.Fixture { QuickSettingsShadeSceneActionsViewModel( - shadeInteractor = shadeInteractor, quickSettingsContainerViewModel = quickSettingsContainerViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt index 5ad5cb28e549..cd1704caad70 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt @@ -14,18 +14,13 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.qs.ui.viewmodel import com.android.systemui.kosmos.Kosmos -import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModelFactory -import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.quickSettingsShadeSceneContentViewModel: QuickSettingsShadeSceneContentViewModel by Kosmos.Fixture { QuickSettingsShadeSceneContentViewModel( - overlayShadeViewModelFactory = overlayShadeViewModelFactory, quickSettingsContainerViewModel = quickSettingsContainerViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 7dfe8027d783..b3664e107d72 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -1,9 +1,8 @@ package com.android.systemui.scene -import com.android.compose.animation.scene.OverlayKey import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.scene.shared.model.FakeScene +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.FakeOverlay @@ -19,15 +18,12 @@ var Kosmos.sceneKeys by Fixture { ) } -val Kosmos.fakeScenes by Fixture { sceneKeys.map { key -> FakeScene(key) }.toSet() } - -val Kosmos.scenes by Fixture { fakeScenes } - val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen } var Kosmos.overlayKeys by Fixture { - listOf<OverlayKey>( - // TODO(b/356596436): Add overlays here when we have them. + listOf( + Overlays.NotificationsShade, + Overlays.QuickSettingsShade, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt index 59f2b9412413..c95b2dcb08b6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent private val mutableTransitionState = MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(Scenes.Lockscreen)) @@ -116,13 +115,13 @@ private fun getStateWithUndefined( is ObservableTransitionState.Transition -> { TransitionStep( from = - if (sceneTransition.fromScene != Scenes.Lockscreen) { + if (sceneTransition.fromContent != Scenes.Lockscreen) { KeyguardState.UNDEFINED } else { state.from }, to = - if (sceneTransition.toScene != Scenes.Lockscreen) { + if (sceneTransition.toContent != Scenes.Lockscreen) { KeyguardState.UNDEFINED } else { state.from diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt deleted file mode 100644 index 78358f5a9187..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeScene.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2024 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.scene.shared.model - -import com.android.compose.animation.scene.SceneKey -import com.android.compose.animation.scene.UserAction -import com.android.compose.animation.scene.UserActionResult -import com.android.systemui.lifecycle.ExclusiveActivatable -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.receiveAsFlow - -class FakeScene( - override val key: SceneKey, -) : ExclusiveActivatable(), Scene { - var isDestinationScenesBeingCollected = false - - private val destinationScenesChannel = Channel<Map<UserAction, UserActionResult>>() - - override val destinationScenes = - destinationScenesChannel - .receiveAsFlow() - .onStart { isDestinationScenesBeingCollected = true } - .onCompletion { isDestinationScenesBeingCollected = false } - - override suspend fun onActivated(): Nothing { - awaitCancellation() - } - - suspend fun setDestinationScenes(value: Map<UserAction, UserActionResult>) { - destinationScenesChannel.send(value) - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt index 4660337c0d4b..4a86fd5e49ff 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt @@ -72,10 +72,6 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead") override val legacyLockscreenShadeTracking = MutableStateFlow(false) - private var _isDualShadeAlignedToBottom = false - override val isDualShadeAlignedToBottom - get() = _isDualShadeAlignedToBottom - private var _isShadeLayoutWide = MutableStateFlow(false) override val isShadeLayoutWide: StateFlow<Boolean> = _isShadeLayoutWide.asStateFlow() @@ -155,10 +151,6 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository { _legacyShadeExpansion.value = expandedFraction } - fun setDualShadeAlignedToBottom(isAlignedToBottom: Boolean) { - _isDualShadeAlignedToBottom = isAlignedToBottom - } - override fun setShadeLayoutWide(isShadeLayoutWide: Boolean) { _isShadeLayoutWide.value = isShadeLayoutWide } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayActionsViewModelKosmos.kt new file mode 100644 index 000000000000..1e00ac4aa46e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayActionsViewModelKosmos.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 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.shade.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel + +val Kosmos.notificationsShadeOverlayActionsViewModel: + NotificationsShadeOverlayActionsViewModel by Fixture { + NotificationsShadeOverlayActionsViewModel() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt new file mode 100644 index 000000000000..9cdd51994262 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 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.shade.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory + +val Kosmos.notificationsShadeOverlayContentViewModel: + NotificationsShadeOverlayContentViewModel by Fixture { + NotificationsShadeOverlayContentViewModel( + shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, + notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory, + sceneInteractor = sceneInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt index 9bf4756f53b0..f792ab95e8ac 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt @@ -19,11 +19,6 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel -import com.android.systemui.shade.domain.interactor.shadeInteractor val Kosmos.notificationsShadeSceneActionsViewModel: - NotificationsShadeSceneActionsViewModel by Fixture { - NotificationsShadeSceneActionsViewModel( - shadeInteractor = shadeInteractor, - ) -} + NotificationsShadeSceneActionsViewModel by Fixture { NotificationsShadeSceneActionsViewModel() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index 634354b033ef..8bfc390ecfa3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -35,3 +35,11 @@ val Kosmos.notificationsPlaceholderViewModel by Fixture { dumpManager = dumpManager, ) } + +val Kosmos.notificationsPlaceholderViewModelFactory by Fixture { + object : NotificationsPlaceholderViewModel.Factory { + override fun create(): NotificationsPlaceholderViewModel { + return notificationsPlaceholderViewModel + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ManagedProfileControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ManagedProfileControllerKosmos.kt new file mode 100644 index 000000000000..ef04b9d907f1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ManagedProfileControllerKosmos.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.phone + +import android.testing.LeakCheck +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.utils.leaks.FakeManagedProfileController +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.fakeManagedProfileController by Fixture { FakeManagedProfileController(LeakCheck()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt index 35fa2af7639f..73d423cc3e7a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeGlobalSettingsKosmos.kt @@ -19,5 +19,10 @@ package com.android.systemui.util.settings import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.unconfinedTestDispatcher val Kosmos.fakeGlobalSettings: FakeGlobalSettings by Fixture { FakeGlobalSettings(testDispatcher) } + +val Kosmos.unconfinedDispatcherFakeGlobalSettings: FakeGlobalSettings by Fixture { + FakeGlobalSettings(unconfinedTestDispatcher) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt index 76ef20253078..e1daf9bdc773 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettingsKosmos.kt @@ -19,8 +19,13 @@ package com.android.systemui.util.settings import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.unconfinedTestDispatcher import com.android.systemui.settings.userTracker val Kosmos.fakeSettings: FakeSettings by Fixture { FakeSettings(testDispatcher) { userTracker.userId } } + +val Kosmos.unconfinedDispatcherFakeSettings: FakeSettings by Fixture { + FakeSettings(unconfinedTestDispatcher) { userTracker.userId } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt index 83adc799b471..ad1292e0af0c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt @@ -38,6 +38,7 @@ object TestMediaDevicesFactory { ): MediaDevice = mock { whenever(name).thenReturn(deviceName) whenever(icon).thenReturn(deviceIcon) + whenever(deviceType).thenReturn(MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE) } fun wiredMediaDevice( @@ -77,6 +78,18 @@ object TestMediaDevicesFactory { whenever(name).thenReturn(deviceName) whenever(icon).thenReturn(deviceIcon) whenever(cachedDevice).thenReturn(cachedBluetoothDevice) + whenever(deviceType).thenReturn(MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE) + } + } + + fun remoteMediaDevice( + deviceName: String = "remote_media", + deviceIcon: Drawable? = TestStubDrawable(), + ): MediaDevice { + return mock<MediaDevice> { + whenever(name).thenReturn(deviceName) + whenever(icon).thenReturn(deviceIcon) + whenever(deviceType).thenReturn(MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE) } } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index 7d991663f4b1..7a1609552ede 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -203,6 +203,10 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde // so record it and throw it when the test actually started. log("Fatal: Exception detected in constructor: " + th.getMessage() + "\n" + Log.getStackTraceString(th)); + if (true) { + // TODO(b/363094647) Remove this + throw th; + } mExceptionInConstructor = new RuntimeException("Exception detected in constructor", th); mDescription = Description.createTestDescription(testClass, "Constructor"); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index f1a8b5a96080..b541345e1cb8 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4903,40 +4903,26 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override - @RequiresNoPermission - public boolean startFlashNotificationSequence(String opPkg, - @FlashNotificationReason int reason, IBinder token) { - final long identity = Binder.clearCallingIdentity(); - try { - return mFlashNotificationsController.startFlashNotificationSequence(opPkg, - reason, token); - } finally { - Binder.restoreCallingIdentity(identity); - } + @EnforcePermission(MANAGE_ACCESSIBILITY) + public boolean startFlashNotificationSequence(String opPkg, @FlashNotificationReason int reason, + IBinder token) { + startFlashNotificationSequence_enforcePermission(); + return mFlashNotificationsController.startFlashNotificationSequence(opPkg, reason, token); } @Override - @RequiresNoPermission + @EnforcePermission(MANAGE_ACCESSIBILITY) public boolean stopFlashNotificationSequence(String opPkg) { - final long identity = Binder.clearCallingIdentity(); - try { - return mFlashNotificationsController.stopFlashNotificationSequence(opPkg); - } finally { - Binder.restoreCallingIdentity(identity); - } + stopFlashNotificationSequence_enforcePermission(); + return mFlashNotificationsController.stopFlashNotificationSequence(opPkg); } @Override - @RequiresNoPermission - public boolean startFlashNotificationEvent(String opPkg, - @FlashNotificationReason int reason, String reasonPkg) { - final long identity = Binder.clearCallingIdentity(); - try { - return mFlashNotificationsController.startFlashNotificationEvent(opPkg, - reason, reasonPkg); - } finally { - Binder.restoreCallingIdentity(identity); - } + @EnforcePermission(MANAGE_ACCESSIBILITY) + public boolean startFlashNotificationEvent(String opPkg, @FlashNotificationReason int reason, + String reasonPkg) { + startFlashNotificationEvent_enforcePermission(); + return mFlashNotificationsController.startFlashNotificationEvent(opPkg, reason, reasonPkg); } @Override diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java index 83f57b2bf31c..950246ff20ac 100644 --- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java +++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerManager.java @@ -87,7 +87,7 @@ public final class AccessibilityCheckerManager { public Set<AndroidAccessibilityCheckerResult> maybeRunA11yChecker( List<AccessibilityNodeInfo> nodes, @Nullable String sourceEventClassName, ComponentName a11yServiceComponentName, @UserIdInt int userId) { - if (!shouldRunA11yChecker()) { + if (!shouldRunA11yChecker() || nodes.isEmpty()) { return Set.of(); } @@ -95,24 +95,33 @@ public final class AccessibilityCheckerManager { String defaultBrowserName = mPackageManager.getDefaultBrowserPackageNameAsUser(userId); try { + AndroidAccessibilityCheckerResult.Builder commonResultBuilder = + AccessibilityCheckerUtils.getCommonResultBuilder(nodes.getFirst(), + sourceEventClassName, mPackageManager, a11yServiceComponentName); + if (commonResultBuilder == null) { + return Set.of(); + } for (AccessibilityNodeInfo nodeInfo : nodes) { - // Skip browser results because they are mostly related to web content and not the - // browser app itself. + // Skip browser results because they are mostly related to web content and + // not the browser app itself. if (nodeInfo.getPackageName() == null || nodeInfo.getPackageName().toString().equals(defaultBrowserName)) { continue; } - List<AccessibilityHierarchyCheckResult> checkResults = runChecksOnNode(nodeInfo); + List<AccessibilityHierarchyCheckResult> checkResults = runChecksOnNode( + nodeInfo); Set<AndroidAccessibilityCheckerResult> filteredResults = AccessibilityCheckerUtils.processResults(nodeInfo, checkResults, - sourceEventClassName, mPackageManager, a11yServiceComponentName); + commonResultBuilder); allResults.addAll(filteredResults); } mCachedResults.addAll(allResults); + return allResults; + } catch (RuntimeException e) { Slog.e(LOG_TAG, "An unknown error occurred while running a11y checker.", e); + return Set.of(); } - return allResults; } private List<AccessibilityHierarchyCheckResult> runChecksOnNode( diff --git a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java index eb24b027dc3a..a739304fc269 100644 --- a/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java +++ b/services/accessibility/java/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtils.java @@ -91,45 +91,55 @@ public class AccessibilityCheckerUtils { AccessibilityCheckClass.TRAVERSAL_ORDER_CHECK)); // LINT.ThenChange(/services/accessibility/java/com/android/server/accessibility/a11ychecker/proto/a11ychecker.proto) - static Set<AndroidAccessibilityCheckerResult> processResults( + + /** + * Returns AccessibilityCheckResultReported.Builder with the common fields for all nodes + * belonging in the same cache pre-filled. + */ + static @Nullable AndroidAccessibilityCheckerResult.Builder getCommonResultBuilder( AccessibilityNodeInfo nodeInfo, - List<AccessibilityHierarchyCheckResult> checkResults, @Nullable String activityClassName, PackageManager packageManager, ComponentName a11yServiceComponentName) { - String appPackageName = nodeInfo.getPackageName().toString(); - String nodePath = AccessibilityNodePathBuilder.createNodePath(nodeInfo); - if (nodePath == null) { - return Set.of(); + if (nodeInfo.getPackageName() == null) { + return null; } - AndroidAccessibilityCheckerResult.Builder commonBuilder; + String appPackageName = nodeInfo.getPackageName().toString(); try { - commonBuilder = AndroidAccessibilityCheckerResult.newBuilder() + return AndroidAccessibilityCheckerResult.newBuilder() .setPackageName(appPackageName) .setAppVersionCode(getAppVersionCode(packageManager, appPackageName)) - .setUiElementPath(nodePath) .setActivityName( getActivityName(packageManager, appPackageName, activityClassName)) .setWindowTitle(getWindowTitle(nodeInfo)) .setSourceComponentName(a11yServiceComponentName) - .setSourceVersionCode( - getAppVersionCode(packageManager, - a11yServiceComponentName.getPackageName())); + .setSourceVersionCode(getAppVersionCode(packageManager, + a11yServiceComponentName.getPackageName())); } catch (PackageManager.NameNotFoundException e) { Slog.e(LOG_TAG, "Unknown package name", e); - return Set.of(); + return null; } + } + static Set<AndroidAccessibilityCheckerResult> processResults( + AccessibilityNodeInfo nodeInfo, + List<AccessibilityHierarchyCheckResult> checkResults, + AndroidAccessibilityCheckerResult.Builder resultBuilder) { + String nodePath = AccessibilityNodePathBuilder.createNodePath(nodeInfo); + if (resultBuilder == null || nodePath == null) { + return Set.of(); + } return checkResults.stream() .filter(checkResult -> checkResult.getType() == AccessibilityCheckResult.AccessibilityCheckResultType.ERROR || checkResult.getType() == AccessibilityCheckResult.AccessibilityCheckResultType.WARNING) - .map(checkResult -> new AndroidAccessibilityCheckerResult.Builder( - commonBuilder).setResultCheckClass( - getCheckClass(checkResult)).setResultType( - getCheckResultType(checkResult)).setResultId( - checkResult.getResultId()).build()) + .map(checkResult -> new AndroidAccessibilityCheckerResult.Builder(resultBuilder) + .setUiElementPath(nodePath) + .setResultCheckClass(getCheckClass(checkResult)) + .setResultType(getCheckResultType(checkResult)) + .setResultId(checkResult.getResultId()) + .build()) .collect(Collectors.toUnmodifiableSet()); } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MouseEventHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/MouseEventHandler.java index 845249e2c82f..ab94e989f8b3 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MouseEventHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MouseEventHandler.java @@ -39,8 +39,14 @@ public final class MouseEventHandler { * @param displayId The display that is being magnified */ public void onEvent(MotionEvent event, int displayId) { - if (event.getAction() == ACTION_HOVER_MOVE - || (event.getAction() == ACTION_MOVE && event.getSource() == SOURCE_MOUSE)) { + // Ignore gesture events synthesized from the touchpad. + // TODO(b/354696546): Use synthesized pinch gestures to control scale. + boolean isSynthesizedFromTouchpad = + event.getClassification() != MotionEvent.CLASSIFICATION_NONE; + + // Consume only move events from the mouse or hovers from any tool. + if (!isSynthesizedFromTouchpad && (event.getAction() == ACTION_HOVER_MOVE + || (event.getAction() == ACTION_MOVE && event.getSource() == SOURCE_MOUSE))) { final float eventX = event.getX(); final float eventY = event.getY(); diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java index 954651d9ef0a..a2d467c2085c 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerService.java @@ -16,8 +16,7 @@ package com.android.server.appfunctions; -import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; - +import android.app.appfunctions.AppFunctionManagerConfiguration; import android.content.Context; import com.android.server.SystemService; @@ -35,7 +34,7 @@ public class AppFunctionManagerService extends SystemService { @Override public void onStart() { - if (enableAppFunctionManager()) { + if (AppFunctionManagerConfiguration.isSupported(getContext())) { publishBinderService(Context.APP_FUNCTION_SERVICE, mServiceImpl); } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index f5d07ef850c8..32b8d6b476c3 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -25,6 +25,7 @@ import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback; import android.content.Context; import android.content.Intent; +import android.os.Binder; import android.os.UserHandle; import android.text.TextUtils; import android.util.Slog; @@ -141,10 +142,16 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { )); return; } - bindAppFunctionServiceUnchecked(requestInternal, serviceIntent, targetUser, + + final long token = Binder.clearCallingIdentity(); + try { + bindAppFunctionServiceUnchecked(requestInternal, serviceIntent, targetUser, safeExecuteAppFunctionCallback, /*bindFlags=*/ Context.BIND_AUTO_CREATE, /*timeoutInMillis=*/ mServiceConfig.getExecuteAppFunctionTimeoutMillis()); + } finally { + Binder.restoreCallingIdentity(token); + } } private void bindAppFunctionServiceUnchecked( diff --git a/services/appfunctions/java/com/android/server/appfunctions/SyncAppSearchCallHelper.java b/services/appfunctions/java/com/android/server/appfunctions/SyncAppSearchCallHelper.java index c01fe311e9ca..5dd4c250af58 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/SyncAppSearchCallHelper.java +++ b/services/appfunctions/java/com/android/server/appfunctions/SyncAppSearchCallHelper.java @@ -38,7 +38,9 @@ import java.util.Objects; import java.util.concurrent.Executor; /** - * Helper class for interacting with a system server local appsearch session synchronously. + * Helper class for interacting with a system server local appsearch session asynchronously. + * + * <p>Converts the AppSearch Callback API to {@link AndroidFuture}. */ @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) public class SyncAppSearchCallHelper implements Closeable { @@ -47,9 +49,10 @@ public class SyncAppSearchCallHelper implements Closeable { private final AppSearchManager mAppSearchManager; private final AndroidFuture<AppSearchResult<AppSearchSession>> mSettableSessionFuture; - public SyncAppSearchCallHelper(@NonNull AppSearchManager appSearchManager, - @NonNull Executor executor, - @NonNull SearchContext appSearchContext) { + public SyncAppSearchCallHelper( + @NonNull AppSearchManager appSearchManager, + @NonNull Executor executor, + @NonNull SearchContext appSearchContext) { Objects.requireNonNull(appSearchManager); Objects.requireNonNull(executor); Objects.requireNonNull(appSearchContext); @@ -61,68 +64,81 @@ public class SyncAppSearchCallHelper implements Closeable { appSearchContext, mExecutor, mSettableSessionFuture::complete); } - /** - * Converts a failed app search result codes into an exception. - */ + /** Converts a failed app search result codes into an exception. */ @NonNull private static Exception failedResultToException(@NonNull AppSearchResult appSearchResult) { return switch (appSearchResult.getResultCode()) { - case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException( - appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_IO_ERROR -> new IOException( - appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException( - appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_INVALID_ARGUMENT -> + new IllegalArgumentException(appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_IO_ERROR -> + new IOException(appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_SECURITY_ERROR -> + new SecurityException(appSearchResult.getErrorMessage()); default -> new IllegalStateException(appSearchResult.getErrorMessage()); }; } - private AppSearchSession getSession() throws Exception { - AppSearchResult<AppSearchSession> sessionResult = mSettableSessionFuture.get(); - if (!sessionResult.isSuccess()) { - throw failedResultToException(sessionResult); - } - return sessionResult.getResultValue(); + private AndroidFuture<AppSearchSession> getSessionAsync() { + return mSettableSessionFuture.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException(failedResultToException(result)); + } + }); } - /** - * Gets the schema for a given app search session. - */ - @WorkerThread - public GetSchemaResponse getSchema() throws Exception { - AndroidFuture<AppSearchResult<GetSchemaResponse>> settableSchemaResponse = - new AndroidFuture<>(); - getSession().getSchema(mExecutor, settableSchemaResponse::complete); - AppSearchResult<GetSchemaResponse> schemaResponse = settableSchemaResponse.get(); - if (schemaResponse.isSuccess()) { - return schemaResponse.getResultValue(); - } else { - throw failedResultToException(schemaResponse); - } + /** Gets the schema for a given app search session. */ + public AndroidFuture<GetSchemaResponse> getSchema() { + return getSessionAsync() + .thenComposeAsync( + session -> { + AndroidFuture<AppSearchResult<GetSchemaResponse>> + settableSchemaResponse = new AndroidFuture<>(); + session.getSchema(mExecutor, settableSchemaResponse::complete); + return settableSchemaResponse.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException( + failedResultToException(result)); + } + }); + }, + mExecutor); } - /** - * Sets the schema for a given app search session. - */ - @WorkerThread - public SetSchemaResponse setSchema( - @NonNull SetSchemaRequest setSchemaRequest) throws Exception { - AndroidFuture<AppSearchResult<SetSchemaResponse>> settableSchemaResponse = - new AndroidFuture<>(); - getSession().setSchema( - setSchemaRequest, mExecutor, mExecutor, settableSchemaResponse::complete); - AppSearchResult<SetSchemaResponse> schemaResponse = settableSchemaResponse.get(); - if (schemaResponse.isSuccess()) { - return schemaResponse.getResultValue(); - } else { - throw failedResultToException(schemaResponse); - } + /** Sets the schema for a given app search session. */ + public AndroidFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest setSchemaRequest) { + return getSessionAsync() + .thenComposeAsync( + session -> { + AndroidFuture<AppSearchResult<SetSchemaResponse>> + settableSchemaResponse = new AndroidFuture<>(); + session.setSchema( + setSchemaRequest, + mExecutor, + mExecutor, + settableSchemaResponse::complete); + return settableSchemaResponse.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException( + failedResultToException(result)); + } + }); + }, + mExecutor); } @Override public void close() throws IOException { try { - getSession().close(); + getSessionAsync().get().close(); } catch (Exception ex) { Slog.e(TAG, "Failed to close app search session", ex); } diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index 2119622bd2d0..4b9065bc7f72 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -27,7 +27,6 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.WindowConfiguration; import android.app.compat.CompatChanges; -import android.companion.virtual.VirtualDeviceManager.ActivityListener; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.AttributionSource; @@ -61,6 +60,9 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController private static final String TAG = "GenericWindowPolicyController"; + private static final ComponentName BLOCKED_APP_STREAMING_COMPONENT = + new ComponentName("android", BlockedAppStreamingActivity.class.getName()); + /** Interface to listen running applications change on virtual display. */ public interface RunningAppsChangedListener { /** @@ -69,29 +71,25 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController void onRunningAppsChanged(ArraySet<Integer> runningUids); } - /** - * For communicating when activities are blocked from running on the display by this policy - * controller. - */ - public interface ActivityBlockedCallback { + /** Interface to react to activity changes on the virtual display. */ + public interface ActivityListener { + + /** Called when the top activity changes. */ + void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity, + @UserIdInt int userId); + + /** Called when the display becomes empty. */ + void onDisplayEmpty(int displayId); + /** Called when an activity is blocked.*/ - void onActivityBlocked(int displayId, ActivityInfo activityInfo, IntentSender intentSender); - } - private static final ComponentName BLOCKED_APP_STREAMING_COMPONENT = - new ComponentName("android", BlockedAppStreamingActivity.class.getName()); + void onActivityLaunchBlocked(int displayId, @NonNull ActivityInfo activityInfo, + @Nullable IntentSender intentSender); - /** - * For communicating when a secure window shows on the virtual display. - */ - public interface SecureWindowCallback { /** Called when a secure window shows on the virtual display. */ - void onSecureWindowShown(int displayId, int uid); - } + void onSecureWindowShown(int displayId, @NonNull ActivityInfo activityInfo); - /** Interface to listen for interception of intents. */ - public interface IntentListenerCallback { /** Returns true when an intent should be intercepted */ - boolean shouldInterceptIntent(Intent intent); + boolean shouldInterceptIntent(@NonNull Intent intent); } /** @@ -118,7 +116,6 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController private final ArraySet<ComponentName> mCrossTaskNavigationExemptions; @NonNull private final Object mGenericWindowPolicyControllerLock = new Object(); - @Nullable private final ActivityBlockedCallback mActivityBlockedCallback; // Do not access mDisplayId and mIsMirrorDisplay directly, instead use waitAndGetDisplayId() // and waitAndGetIsMirrorDisplay() @@ -129,14 +126,12 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @NonNull @GuardedBy("mGenericWindowPolicyControllerLock") private final ArraySet<Integer> mRunningUids = new ArraySet<>(); - @Nullable private final ActivityListener mActivityListener; - @Nullable private final IntentListenerCallback mIntentListenerCallback; + @NonNull private final ActivityListener mActivityListener; private final Handler mHandler = new Handler(Looper.getMainLooper()); @NonNull @GuardedBy("mGenericWindowPolicyControllerLock") private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListeners = new ArraySet<>(); - @Nullable private final SecureWindowCallback mSecureWindowCallback; @NonNull private final Set<String> mDisplayCategories; @GuardedBy("mGenericWindowPolicyControllerLock") @@ -162,12 +157,6 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController * @param crossTaskNavigationExemptions The set of components explicitly exempt from the default * navigation policy. * @param activityListener Activity listener to listen for activity changes. - * @param activityBlockedCallback Callback that is called when an activity is blocked from - * launching. - * @param secureWindowCallback Callback that is called when a secure window shows on the - * virtual display. - * @param intentListenerCallback Callback that is called to intercept intents when matching - * passed in filters. * @param showTasksInHostDeviceRecents whether to show activities in recents on the host device. * @param customHomeComponent The component acting as a home activity on the virtual display. If * {@code null}, then the system-default secondary home activity will be used. This is only @@ -184,10 +173,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @NonNull Set<String> activityPolicyPackageExemptions, boolean crossTaskNavigationAllowedByDefault, @NonNull Set<ComponentName> crossTaskNavigationExemptions, - @Nullable ActivityListener activityListener, - @Nullable ActivityBlockedCallback activityBlockedCallback, - @Nullable SecureWindowCallback secureWindowCallback, - @Nullable IntentListenerCallback intentListenerCallback, + @NonNull ActivityListener activityListener, @NonNull Set<String> displayCategories, boolean showTasksInHostDeviceRecents, @Nullable ComponentName customHomeComponent) { @@ -199,11 +185,8 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController mActivityPolicyPackageExemptions = new ArraySet<>(activityPolicyPackageExemptions); mCrossTaskNavigationAllowedByDefault = crossTaskNavigationAllowedByDefault; mCrossTaskNavigationExemptions = new ArraySet<>(crossTaskNavigationExemptions); - mActivityBlockedCallback = activityBlockedCallback; setInterestedWindowFlags(windowFlags, systemWindowFlags); mActivityListener = activityListener; - mSecureWindowCallback = secureWindowCallback; - mIntentListenerCallback = intentListenerCallback; mDisplayCategories = displayCategories; mShowTasksInHostDeviceRecents = showTasksInHostDeviceRecents; mCustomHomeComponent = customHomeComponent; @@ -306,8 +289,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController @Nullable Intent intent, @WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId, boolean isNewTask, boolean isResultExpected, @Nullable Supplier<IntentSender> intentSender) { - if (mIntentListenerCallback != null && intent != null - && mIntentListenerCallback.shouldInterceptIntent(intent)) { + if (intent != null && mActivityListener.shouldInterceptIntent(intent)) { logActivityLaunchBlocked("Virtual device intercepting intent"); return false; } @@ -391,11 +373,9 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController int displayId = waitAndGetDisplayId(); // The callback is fired only when windowFlags are changed. To let VirtualDevice owner // aware that the virtual display has a secure window on top. - if ((windowFlags & FLAG_SECURE) != 0 && mSecureWindowCallback != null - && displayId != INVALID_DISPLAY) { + if ((windowFlags & FLAG_SECURE) != 0 && displayId != INVALID_DISPLAY) { // Post callback on the main thread, so it doesn't block activity launching. - mHandler.post(() -> mSecureWindowCallback.onSecureWindowShown(displayId, - activityInfo.applicationInfo.uid)); + mHandler.post(() -> mActivityListener.onSecureWindowShown(displayId, activityInfo)); } if (!CompatChanges.isChangeEnabled(ALLOW_SECURE_ACTIVITY_DISPLAY_ON_REMOTE_DEVICE, @@ -418,7 +398,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController // Don't send onTopActivityChanged() callback when topActivity is null because it's defined // as @NonNull in ActivityListener interface. Sends onDisplayEmpty() callback instead when // there is no activity running on virtual display. - if (mActivityListener != null && topActivity != null && displayId != INVALID_DISPLAY) { + if (topActivity != null && displayId != INVALID_DISPLAY) { // Post callback on the main thread so it doesn't block activity launching mHandler.post(() -> mActivityListener.onTopActivityChanged(displayId, topActivity, userId)); @@ -431,8 +411,7 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController mRunningUids.clear(); mRunningUids.addAll(runningUids); int displayId = waitAndGetDisplayId(); - if (mActivityListener != null && mRunningUids.isEmpty() - && displayId != INVALID_DISPLAY) { + if (mRunningUids.isEmpty() && displayId != INVALID_DISPLAY) { // Post callback on the main thread so it doesn't block activity launching mHandler.post(() -> mActivityListener.onDisplayEmpty(displayId)); } @@ -482,9 +461,8 @@ public class GenericWindowPolicyController extends DisplayWindowPolicyController int displayId = waitAndGetDisplayId(); // Don't trigger activity blocked callback for mirror displays, because we can't show // any activity or presentation on it anyway. - if (!waitAndGetIsMirrorDisplay() && mActivityBlockedCallback != null - && displayId != INVALID_DISPLAY) { - mActivityBlockedCallback.onActivityBlocked(displayId, activityInfo, + if (!waitAndGetIsMirrorDisplay() && displayId != INVALID_DISPLAY) { + mActivityListener.onActivityLaunchBlocked(displayId, activityInfo, intentSender == null ? null : intentSender.get()); } Counter.logIncrementWithUid( 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 4eb50a952c04..cd2dd3a27c9a 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -40,6 +40,7 @@ import android.app.Activity; import android.app.ActivityOptions; import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; +import android.app.compat.CompatChanges; import android.companion.AssociationInfo; import android.companion.virtual.ActivityPolicyExemption; import android.companion.virtual.IVirtualDevice; @@ -48,7 +49,6 @@ import android.companion.virtual.IVirtualDeviceIntentInterceptor; import android.companion.virtual.IVirtualDeviceSoundEffectListener; import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; -import android.companion.virtual.VirtualDeviceManager.ActivityListener; import android.companion.virtual.VirtualDeviceParams; import android.companion.virtual.audio.IAudioConfigChangedCallback; import android.companion.virtual.audio.IAudioRoutingCallback; @@ -56,6 +56,8 @@ import android.companion.virtual.camera.VirtualCameraConfig; import android.companion.virtual.flags.Flags; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorEvent; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; import android.content.AttributionSource; import android.content.ComponentName; import android.content.Context; @@ -88,6 +90,7 @@ import android.hardware.input.VirtualTouchscreenConfig; import android.media.AudioManager; import android.media.audiopolicy.AudioMix; import android.os.Binder; +import android.os.Build; import android.os.IBinder; import android.os.LocaleList; import android.os.Looper; @@ -132,6 +135,16 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private static final String TAG = "VirtualDeviceImpl"; + /** + * Do not show a toast on the virtual display when a secure surface is shown after + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}. VDM clients should use + * {@link VirtualDeviceManager.ActivityListener#onSecureWindowShown} instead to provide + * a custom notification if desired. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + public static final long DO_NOT_SHOW_TOAST_WHEN_SECURE_SURFACE_SHOWN = 311101667L; + private static final int DEFAULT_VIRTUAL_DISPLAY_FLAGS = DisplayManager.VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED | DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL @@ -182,7 +195,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @GuardedBy("mVirtualDeviceLock") private final SparseArray<VirtualDisplayWrapper> mVirtualDisplays = new SparseArray<>(); private IVirtualDeviceActivityListener mActivityListener; - private ActivityListener mActivityListenerAdapter = null; + private GenericWindowPolicyController.ActivityListener mActivityListenerAdapter = null; private IVirtualDeviceSoundEffectListener mSoundEffectListener; private final DisplayManagerGlobal mDisplayManager; private final DisplayManagerInternal mDisplayManagerInternal; @@ -207,50 +220,122 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @NonNull private final Set<String> mActivityPolicyPackageExemptions = new ArraySet<>(); - private ActivityListener createListenerAdapter() { - return new ActivityListener() { + private class GwpcActivityListener implements GenericWindowPolicyController.ActivityListener { - @Override - public void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity) { - try { - mActivityListener.onTopActivityChanged(displayId, topActivity, - UserHandle.USER_NULL); - } catch (RemoteException e) { - Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); - } + @Override + public void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity, + @UserIdInt int userId) { + try { + mActivityListener.onTopActivityChanged(displayId, topActivity, userId); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); + } + } + + @Override + public void onDisplayEmpty(int displayId) { + try { + mActivityListener.onDisplayEmpty(displayId); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); } + } - @Override - public void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity, - @UserIdInt int userId) { + @Override + public void onActivityLaunchBlocked(int displayId, @NonNull ActivityInfo activityInfo, + @Nullable IntentSender intentSender) { + Intent intent = + BlockedAppStreamingActivity.createIntent(activityInfo, getDisplayName()); + if (shouldShowBlockedActivityDialog( + activityInfo.getComponentName(), intent.getComponent())) { + mContext.startActivityAsUser( + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK), + ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(), + UserHandle.SYSTEM); + } + + if (android.companion.virtualdevice.flags.Flags.activityControlApi()) { try { - mActivityListener.onTopActivityChanged(displayId, topActivity, userId); + mActivityListener.onActivityLaunchBlocked( + displayId, + activityInfo.getComponentName(), + UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid), + intentSender); } catch (RemoteException e) { Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); } } + } - @Override - public void onDisplayEmpty(int displayId) { + @Override + public void onSecureWindowShown(int displayId, @NonNull ActivityInfo activityInfo) { + if (android.companion.virtualdevice.flags.Flags.activityControlApi()) { try { - mActivityListener.onDisplayEmpty(displayId); + mActivityListener.onSecureWindowShown( + displayId, + activityInfo.getComponentName(), + UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid)); } catch (RemoteException e) { Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); } + + if (CompatChanges.isChangeEnabled(DO_NOT_SHOW_TOAST_WHEN_SECURE_SURFACE_SHOWN, + mOwnerPackageName, UserHandle.getUserHandleForUid(mOwnerUid))) { + return; + } } - @Override - public void onActivityLaunchBlocked(int displayId, - @NonNull ComponentName componentName, @NonNull UserHandle user, - @Nullable IntentSender intentSender) { - try { - mActivityListener.onActivityLaunchBlocked( - displayId, componentName, user, intentSender); - } catch (RemoteException e) { - Slog.w(TAG, "Unable to call mActivityListener for display: " + displayId, e); + // If a virtual display isn't secure, the screen can't be captured. Show a warning toast + // if the secure window is shown on a non-secure virtual display. + DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + Display display = displayManager.getDisplay(displayId); + if ((display.getFlags() & Display.FLAG_SECURE) == 0) { + showToastWhereUidIsRunning(activityInfo.applicationInfo.uid, + com.android.internal.R.string.vdm_secure_window, + Toast.LENGTH_LONG, mContext.getMainLooper()); + + Counter.logIncrementWithUid( + "virtual_devices.value_secure_window_blocked_count", + mAttributionSource.getUid()); + } + } + + /** + * Intercepts intent when matching any of the IntentFilter of any interceptor. Returns true + * if the intent matches any filter notifying the DisplayPolicyController to abort the + * activity launch to be replaced by the interception. + */ + @Override + public boolean shouldInterceptIntent(@NonNull Intent intent) { + synchronized (mVirtualDeviceLock) { + boolean hasInterceptedIntent = false; + for (Map.Entry<IBinder, IntentFilter> interceptor + : mIntentInterceptors.entrySet()) { + IntentFilter intentFilter = interceptor.getValue(); + // Explicitly match the actions because the intent filter will match any intent + // without an explicit action. If the intent has no action, then require that + // there are no actions specified in the filter either. + boolean explicitActionMatch = + intent.getAction() != null || intentFilter.countActions() == 0; + if (explicitActionMatch && intentFilter.match( + intent.getAction(), intent.getType(), intent.getScheme(), + intent.getData(), intent.getCategories(), TAG) >= 0) { + try { + // For privacy reasons, only returning the intents action and data. + // Any other required field will require a review. + IVirtualDeviceIntentInterceptor.Stub.asInterface(interceptor.getKey()) + .onIntentIntercepted( + new Intent(intent.getAction(), intent.getData())); + hasInterceptedIntent = true; + } catch (RemoteException e) { + Slog.w(TAG, "Unable to call mActivityListener", e); + } + } } + return hasInterceptedIntent; } - }; + } } VirtualDeviceImpl( @@ -1290,7 +1375,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub Flags.vdmCustomHome() ? mParams.getHomeComponent() : null; if (mActivityListenerAdapter == null) { - mActivityListenerAdapter = createListenerAdapter(); + mActivityListenerAdapter = new GwpcActivityListener(); } final GenericWindowPolicyController gwpc = new GenericWindowPolicyController( @@ -1306,9 +1391,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub ? mParams.getBlockedCrossTaskNavigations() : mParams.getAllowedCrossTaskNavigations(), mActivityListenerAdapter, - this::onActivityBlocked, - this::onSecureWindowShown, - this::shouldInterceptIntent, displayCategories, showTasksInHostDeviceRecents, homeComponent); @@ -1378,28 +1460,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } - @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) - private void onActivityBlocked(int displayId, ActivityInfo activityInfo, - IntentSender intentSender) { - Intent intent = BlockedAppStreamingActivity.createIntent(activityInfo, getDisplayName()); - if (shouldShowBlockedActivityDialog( - activityInfo.getComponentName(), intent.getComponent())) { - mContext.startActivityAsUser( - intent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK), - ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(), - UserHandle.SYSTEM); - } - - if (android.companion.virtualdevice.flags.Flags.activityControlApi()) { - mActivityListenerAdapter.onActivityLaunchBlocked( - displayId, - activityInfo.getComponentName(), - UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid), - intentSender); - } - } - private boolean shouldShowBlockedActivityDialog(ComponentName blockedComponent, ComponentName blockedAppStreamingActivityComponent) { if (Objects.equals(blockedComponent, blockedAppStreamingActivityComponent)) { @@ -1414,27 +1474,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return getDevicePolicy(POLICY_TYPE_BLOCKED_ACTIVITY) == DEVICE_POLICY_DEFAULT; } - private void onSecureWindowShown(int displayId, int uid) { - synchronized (mVirtualDeviceLock) { - if (!mVirtualDisplays.contains(displayId)) { - return; - } - } - - // If a virtual display isn't secure, the screen can't be captured. Show a warning toast - // if the secure window is shown on a non-secure virtual display. - DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - Display display = displayManager.getDisplay(displayId); - if ((display.getFlags() & Display.FLAG_SECURE) == 0) { - showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_secure_window, - Toast.LENGTH_LONG, mContext.getMainLooper()); - - Counter.logIncrementWithUid( - "virtual_devices.value_secure_window_blocked_count", - mAttributionSource.getUid()); - } - } - private ArraySet<UserHandle> getAllowedUserHandles() { ArraySet<UserHandle> result = new ArraySet<>(); final long token = Binder.clearCallingIdentity(); @@ -1621,40 +1660,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } - /** - * Intercepts intent when matching any of the IntentFilter of any interceptor. Returns true if - * the intent matches any filter notifying the DisplayPolicyController to abort the - * activity launch to be replaced by the interception. - */ - private boolean shouldInterceptIntent(Intent intent) { - synchronized (mVirtualDeviceLock) { - boolean hasInterceptedIntent = false; - for (Map.Entry<IBinder, IntentFilter> interceptor : mIntentInterceptors.entrySet()) { - IntentFilter intentFilter = interceptor.getValue(); - // Explicitly match the actions because the intent filter will match any intent - // without an explicit action. If the intent has no action, then require that there - // are no actions specified in the filter either. - boolean explicitActionMatch = - intent.getAction() != null || intentFilter.countActions() == 0; - if (explicitActionMatch && intentFilter.match( - intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(), - intent.getCategories(), TAG) >= 0) { - try { - // For privacy reasons, only returning the intents action and data. Any - // other required field will require a review. - IVirtualDeviceIntentInterceptor.Stub.asInterface(interceptor.getKey()) - .onIntentIntercepted(new Intent(intent.getAction(), intent.getData())); - hasInterceptedIntent = true; - } catch (RemoteException e) { - Slog.w(TAG, "Unable to call mVirtualDeviceIntentInterceptor", e); - } - } - } - - return hasInterceptedIntent; - } - } - interface PendingTrampolineCallback { /** * Called when the callback should start waiting for the given pending trampoline. diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index d86bae19f174..07e5f2e34ab8 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -224,9 +224,6 @@ class StorageManagerService extends IStorageManager.Stub /** Extended timeout for the system server watchdog for vold#partition operation. */ private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000; - private static final Pattern OBB_FILE_PATH = Pattern.compile( - "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/obb/)([^/]+)/([^/]+\\.obb)"); - @GuardedBy("mLock") private final Set<Integer> mFuseMountedUser = new ArraySet<>(); @@ -3147,9 +3144,7 @@ class StorageManagerService extends IStorageManager.Stub Objects.requireNonNull(rawPath, "rawPath cannot be null"); Objects.requireNonNull(canonicalPath, "canonicalPath cannot be null"); Objects.requireNonNull(token, "token cannot be null"); - Objects.requireNonNull(obbInfo, "obbInfo cannot be null"); - - validateObbInfo(obbInfo, rawPath); + Objects.requireNonNull(obbInfo, "obbIfno cannot be null"); final int callingUid = Binder.getCallingUid(); final ObbState obbState = new ObbState(rawPath, canonicalPath, @@ -3161,34 +3156,6 @@ class StorageManagerService extends IStorageManager.Stub Slog.i(TAG, "Send to OBB handler: " + action.toString()); } - private void validateObbInfo(ObbInfo obbInfo, String rawPath) { - String obbFilePath; - try { - obbFilePath = new File(rawPath).getCanonicalPath(); - } catch (IOException ex) { - throw new RuntimeException("Failed to resolve path" + rawPath + " : " + ex); - } - - Matcher matcher = OBB_FILE_PATH.matcher(obbFilePath); - - if (matcher.matches()) { - int userId = UserHandle.getUserId(Binder.getCallingUid()); - String pathUserId = matcher.group(2); - String pathPackageName = matcher.group(3); - if ((pathUserId != null && Integer.parseInt(pathUserId) != userId) - || (pathUserId == null && userId != mCurrentUserId)) { - throw new SecurityException( - "Path " + obbFilePath + "does not correspond to calling userId " + userId); - } - if (obbInfo != null && !obbInfo.packageName.equals(pathPackageName)) { - throw new SecurityException("Path " + obbFilePath - + " does not contain package name " + pathPackageName); - } - } else { - throw new SecurityException("Invalid path to Obb file : " + obbFilePath); - } - } - @Override public void unmountObb(String rawPath, boolean force, IObbActionListener token, int nonce) { Objects.requireNonNull(rawPath, "rawPath cannot be null"); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 168ec052e67d..4e24cf38fe73 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -790,6 +790,7 @@ public class AudioService extends IAudioService.Stub private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver(); private final Executor mAudioServerLifecycleExecutor; + private long mSysPropListenerNativeHandle; private final List<Future> mScheduledPermissionTasks = new ArrayList(); private IMediaProjectionManager mProjectionService; // to validate projection token @@ -10640,7 +10641,7 @@ public class AudioService extends IAudioService.Stub }, UPDATE_DELAY_MS, TimeUnit.MILLISECONDS)); } }; - mAudioSystem.listenForSystemPropertyChange( + mSysPropListenerNativeHandle = mAudioSystem.listenForSystemPropertyChange( PermissionManager.CACHE_KEY_PACKAGE_INFO, task); } else { @@ -14713,6 +14714,7 @@ public class AudioService extends IAudioService.Stub @Override /** @see AudioManager#permissionUpdateBarrier() */ public void permissionUpdateBarrier() { + mAudioSystem.triggerSystemPropertyUpdate(mSysPropListenerNativeHandle); List<Future> snapshot; synchronized (mScheduledPermissionTasks) { snapshot = List.copyOf(mScheduledPermissionTasks); diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index d083c68c4c2c..5cabddea9c17 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -748,8 +748,12 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, return AudioSystem.setMasterMute(mute); } - public void listenForSystemPropertyChange(String systemPropertyName, Runnable callback) { - AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback); + public long listenForSystemPropertyChange(String systemPropertyName, Runnable callback) { + return AudioSystem.listenForSystemPropertyChange(systemPropertyName, callback); + } + + public void triggerSystemPropertyUpdate(long handle) { + AudioSystem.triggerSystemPropertyUpdate(handle); } /** diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java index f5af5ea3eab4..bc58501f5b59 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java @@ -78,7 +78,6 @@ public class BiometricFrameworkStatsLogger { public void authenticate(OperationContextExt operationContext, int statsModality, int statsAction, int statsClient, boolean isDebug, long latency, int authState, boolean requireConfirmation, int targetUserId, float ambientLightLux) { - Slog.d(TAG, "authenticate logging " + operationContext.getIsMandatoryBiometrics()); FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_AUTHENTICATED, statsModality, targetUserId, @@ -131,7 +130,6 @@ public class BiometricFrameworkStatsLogger { public void error(OperationContextExt operationContext, int statsModality, int statsAction, int statsClient, boolean isDebug, long latency, int error, int vendorCode, int targetUserId) { - Slog.d(TAG, "error logging " + operationContext.getIsMandatoryBiometrics()); FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_ERROR_OCCURRED, statsModality, targetUserId, diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 240e91bccaca..907e7c639352 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -45,6 +45,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; import android.util.EventLog; +import android.util.IndentingPrintWriter; import android.util.MathUtils; import android.util.Slog; import android.util.SparseArray; @@ -562,6 +563,7 @@ public class AutomaticBrightnessController { public void resetShortTermModel() { mCurrentBrightnessMapper.clearUserDataPoints(); mShortTermModel.reset(); + Slog.i(TAG, "Resetting short term model"); } public boolean setBrightnessConfiguration(BrightnessConfiguration configuration, @@ -598,74 +600,79 @@ public class AutomaticBrightnessController { } public void dump(PrintWriter pw) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw); + ipw.increaseIndent(); pw.println(); pw.println("Automatic Brightness Controller Configuration:"); - pw.println(" mState=" + configStateToString(mState)); - pw.println(" mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum); - pw.println(" mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum); - pw.println(" mDozeScaleFactor=" + mDozeScaleFactor); - pw.println(" mInitialLightSensorRate=" + mInitialLightSensorRate); - pw.println(" mNormalLightSensorRate=" + mNormalLightSensorRate); - pw.println(" mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig); - pw.println(" mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig); - pw.println(" mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig); - pw.println(" mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle); - pw.println(" mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle); - pw.println(" mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig); - pw.println(" mAmbientLightHorizonLong=" + mAmbientLightHorizonLong); - pw.println(" mAmbientLightHorizonShort=" + mAmbientLightHorizonShort); - pw.println(" mWeightingIntercept=" + mWeightingIntercept); + pw.println("----------------------------------------------"); + ipw.println("mState=" + configStateToString(mState)); + ipw.println("mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum); + ipw.println("mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum); + ipw.println("mDozeScaleFactor=" + mDozeScaleFactor); + ipw.println("mInitialLightSensorRate=" + mInitialLightSensorRate); + ipw.println("mNormalLightSensorRate=" + mNormalLightSensorRate); + ipw.println("mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig); + ipw.println("mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig); + ipw.println("mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig); + ipw.println("mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle); + ipw.println("mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle); + ipw.println("mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig); + ipw.println("mAmbientLightHorizonLong=" + mAmbientLightHorizonLong); + ipw.println("mAmbientLightHorizonShort=" + mAmbientLightHorizonShort); + ipw.println("mWeightingIntercept=" + mWeightingIntercept); pw.println(); pw.println("Automatic Brightness Controller State:"); - pw.println(" mLightSensor=" + mLightSensor); - pw.println(" mLightSensorEnabled=" + mLightSensorEnabled); - pw.println(" mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime)); - pw.println(" mCurrentLightSensorRate=" + mCurrentLightSensorRate); - pw.println(" mAmbientLux=" + mAmbientLux); - pw.println(" mAmbientLuxValid=" + mAmbientLuxValid); - pw.println(" mPreThresholdLux=" + mPreThresholdLux); - pw.println(" mPreThresholdBrightness=" + mPreThresholdBrightness); - pw.println(" mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold); - pw.println(" mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold); - pw.println(" mScreenBrighteningThreshold=" + mScreenBrighteningThreshold); - pw.println(" mScreenDarkeningThreshold=" + mScreenDarkeningThreshold); - pw.println(" mLastObservedLux=" + mLastObservedLux); - pw.println(" mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime)); - pw.println(" mRecentLightSamples=" + mRecentLightSamples); - pw.println(" mAmbientLightRingBuffer=" + mAmbientLightRingBuffer); - pw.println(" mScreenAutoBrightness=" + mScreenAutoBrightness); - pw.println(" mDisplayPolicy=" + DisplayPowerRequest.policyToString(mDisplayPolicy)); - pw.println(" mShortTermModel="); - mShortTermModel.dump(pw); - pw.println(" mPausedShortTermModel="); - mPausedShortTermModel.dump(pw); - - pw.println(); - pw.println(" mBrightnessAdjustmentSamplePending=" + mBrightnessAdjustmentSamplePending); - pw.println(" mBrightnessAdjustmentSampleOldLux=" + mBrightnessAdjustmentSampleOldLux); - pw.println(" mBrightnessAdjustmentSampleOldBrightness=" + pw.println("--------------------------------------"); + ipw.println("mLightSensor=" + mLightSensor); + ipw.println("mLightSensorEnabled=" + mLightSensorEnabled); + ipw.println("mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime)); + ipw.println("mCurrentLightSensorRate=" + mCurrentLightSensorRate); + ipw.println("mAmbientLux=" + mAmbientLux); + ipw.println("mAmbientLuxValid=" + mAmbientLuxValid); + ipw.println("mPreThresholdLux=" + mPreThresholdLux); + ipw.println("mPreThresholdBrightness=" + mPreThresholdBrightness); + ipw.println("mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold); + ipw.println("mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold); + ipw.println("mScreenBrighteningThreshold=" + mScreenBrighteningThreshold); + ipw.println("mScreenDarkeningThreshold=" + mScreenDarkeningThreshold); + ipw.println("mLastObservedLux=" + mLastObservedLux); + ipw.println("mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime)); + ipw.println("mRecentLightSamples=" + mRecentLightSamples); + ipw.println("mAmbientLightRingBuffer=" + mAmbientLightRingBuffer); + ipw.println("mScreenAutoBrightness=" + mScreenAutoBrightness); + ipw.println("mDisplayPolicy=" + DisplayPowerRequest.policyToString(mDisplayPolicy)); + ipw.println("mShortTermModel="); + + mShortTermModel.dump(ipw); + ipw.println("mPausedShortTermModel="); + mPausedShortTermModel.dump(ipw); + + ipw.println(); + ipw.println("mBrightnessAdjustmentSamplePending=" + mBrightnessAdjustmentSamplePending); + ipw.println("mBrightnessAdjustmentSampleOldLux=" + mBrightnessAdjustmentSampleOldLux); + ipw.println("mBrightnessAdjustmentSampleOldBrightness=" + mBrightnessAdjustmentSampleOldBrightness); - pw.println(" mForegroundAppPackageName=" + mForegroundAppPackageName); - pw.println(" mPendingForegroundAppPackageName=" + mPendingForegroundAppPackageName); - pw.println(" mForegroundAppCategory=" + mForegroundAppCategory); - pw.println(" mPendingForegroundAppCategory=" + mPendingForegroundAppCategory); - pw.println(" Current mode=" + ipw.println("mForegroundAppPackageName=" + mForegroundAppPackageName); + ipw.println("mPendingForegroundAppPackageName=" + mPendingForegroundAppPackageName); + ipw.println("mForegroundAppCategory=" + mForegroundAppCategory); + ipw.println("mPendingForegroundAppCategory=" + mPendingForegroundAppCategory); + ipw.println("Current mode=" + autoBrightnessModeToString(mCurrentBrightnessMapper.getMode())); for (int i = 0; i < mBrightnessMappingStrategyMap.size(); i++) { - pw.println(); - pw.println(" Mapper for mode " + ipw.println(); + ipw.println("Mapper for mode " + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + ":"); - mBrightnessMappingStrategyMap.valueAt(i).dump(pw, + mBrightnessMappingStrategyMap.valueAt(i).dump(ipw, mBrightnessRangeController.getNormalBrightnessMax()); } - pw.println(); - pw.println(" mAmbientBrightnessThresholds=" + mAmbientBrightnessThresholds); - pw.println(" mAmbientBrightnessThresholdsIdle=" + mAmbientBrightnessThresholdsIdle); - pw.println(" mScreenBrightnessThresholds=" + mScreenBrightnessThresholds); - pw.println(" mScreenBrightnessThresholdsIdle=" + mScreenBrightnessThresholdsIdle); + ipw.println(); + ipw.println("mAmbientBrightnessThresholds=" + mAmbientBrightnessThresholds); + ipw.println("mAmbientBrightnessThresholdsIdle=" + mAmbientBrightnessThresholdsIdle); + ipw.println("mScreenBrightnessThresholds=" + mScreenBrightnessThresholds); + ipw.println("mScreenBrightnessThresholdsIdle=" + mScreenBrightnessThresholdsIdle); } public float[] getLastSensorValues() { @@ -1339,8 +1346,10 @@ public class AutomaticBrightnessController { + "\n mIsValid: " + mIsValid; } - void dump(PrintWriter pw) { - pw.println(this); + void dump(IndentingPrintWriter ipw) { + ipw.increaseIndent(); + ipw.println(this); + ipw.decreaseIndent(); } } diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index b0507fb78a41..6a019f3d024c 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -1073,7 +1073,9 @@ public abstract class BrightnessMappingStrategy { pw.println(" mBrightnessRangeAdjustmentApplied=" + mBrightnessRangeAdjustmentApplied); pw.println(" shortTermModelTimeout=" + getShortTermModelTimeout()); - pw.println(" Previous short-term models (oldest to newest): "); + if (!mPreviousBrightnessSplines.isEmpty()) { + pw.println(" Previous short-term models (oldest to newest): "); + } for (int i = 0; i < mPreviousBrightnessSplines.size(); i++) { pw.println(" Computed at " + FORMAT.format(new Date(mBrightnessSplineChangeTimes.get(i))) + ": "); diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java index 631e7518b746..b56a23416693 100644 --- a/services/core/java/com/android/server/display/BrightnessThrottler.java +++ b/services/core/java/com/android/server/display/BrightnessThrottler.java @@ -265,6 +265,7 @@ class BrightnessThrottler { private void dumpLocal(PrintWriter pw) { pw.println("BrightnessThrottler:"); + pw.println("--------------------"); pw.println(" mThermalBrightnessThrottlingDataId=" + mThermalBrightnessThrottlingDataId); pw.println(" mThermalThrottlingData=" + mThermalThrottlingData); pw.println(" mUniqueDisplayId=" + mUniqueDisplayId); diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java index ac5dd203fff6..0f65360e9ee4 100644 --- a/services/core/java/com/android/server/display/BrightnessTracker.java +++ b/services/core/java/com/android/server/display/BrightnessTracker.java @@ -782,6 +782,7 @@ public class BrightnessTracker { public void dump(final PrintWriter pw) { pw.println("BrightnessTracker state:"); + pw.println("------------------------"); synchronized (mDataCollectionLock) { pw.println(" mStarted=" + mStarted); pw.println(" mLightSensor=" + mLightSensor); diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java index 146810f5e1e6..4c133ff035a8 100644 --- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java +++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java @@ -95,6 +95,7 @@ class DeviceStateToLayoutMap { public void dumpLocked(IndentingPrintWriter ipw) { ipw.println("DeviceStateToLayoutMap:"); + ipw.println("-----------------------"); ipw.increaseIndent(); ipw.println("mIsPortInDisplayLayoutEnabled=" + mIsPortInDisplayLayoutEnabled); diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java index dc611fc1d25b..334dda079e7a 100644 --- a/services/core/java/com/android/server/display/DisplayBrightnessState.java +++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java @@ -16,6 +16,7 @@ package com.android.server.display; +import android.hardware.display.BrightnessInfo; import android.text.TextUtils; import com.android.server.display.brightness.BrightnessEvent; @@ -50,6 +51,8 @@ public final class DisplayBrightnessState { private final boolean mIsUserInitiatedChange; + private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason; + private DisplayBrightnessState(Builder builder) { mBrightness = builder.getBrightness(); mHdrBrightness = builder.getHdrBrightness(); @@ -64,6 +67,7 @@ public final class DisplayBrightnessState { mBrightnessEvent = builder.getBrightnessEvent(); mBrightnessAdjustmentFlag = builder.getBrightnessAdjustmentFlag(); mIsUserInitiatedChange = builder.isUserInitiatedChange(); + mBrightnessMaxReason = builder.getBrightnessMaxReason(); } /** @@ -159,6 +163,13 @@ public final class DisplayBrightnessState { return mIsUserInitiatedChange; } + /** + * Gets reason for max brightness restriction + */ + public @BrightnessInfo.BrightnessMaxReason int getBrightnessMaxReason() { + return mBrightnessMaxReason; + } + @Override public String toString() { StringBuilder stringBuilder = new StringBuilder("DisplayBrightnessState:"); @@ -180,6 +191,8 @@ public final class DisplayBrightnessState { .append(Objects.toString(mBrightnessEvent, "null")); stringBuilder.append("\n mBrightnessAdjustmentFlag:").append(mBrightnessAdjustmentFlag); stringBuilder.append("\n mIsUserInitiatedChange:").append(mIsUserInitiatedChange); + stringBuilder.append("\n mBrightnessMaxReason:") + .append(BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason)); return stringBuilder.toString(); } @@ -212,7 +225,8 @@ public final class DisplayBrightnessState { == otherState.shouldUpdateScreenBrightnessSetting() && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent()) && mBrightnessAdjustmentFlag == otherState.getBrightnessAdjustmentFlag() - && mIsUserInitiatedChange == otherState.isUserInitiatedChange(); + && mIsUserInitiatedChange == otherState.isUserInitiatedChange() + && mBrightnessMaxReason == otherState.getBrightnessMaxReason(); } @Override @@ -221,7 +235,7 @@ public final class DisplayBrightnessState { mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness, mCustomAnimationRate, mShouldUpdateScreenBrightnessSetting, mBrightnessEvent, mBrightnessAdjustmentFlag, - mIsUserInitiatedChange); + mIsUserInitiatedChange, mBrightnessMaxReason); } /** @@ -245,12 +259,11 @@ public final class DisplayBrightnessState { private float mMinBrightness; private float mCustomAnimationRate = CUSTOM_ANIMATION_RATE_NOT_SET; private boolean mShouldUpdateScreenBrightnessSetting; - private BrightnessEvent mBrightnessEvent; - - public int mBrightnessAdjustmentFlag = 0; - + private int mBrightnessAdjustmentFlag = 0; private boolean mIsUserInitiatedChange; + private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason = + BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; /** * Create a builder starting with the values from the specified {@link @@ -274,6 +287,7 @@ public final class DisplayBrightnessState { builder.setBrightnessEvent(state.getBrightnessEvent()); builder.setBrightnessAdjustmentFlag(state.getBrightnessAdjustmentFlag()); builder.setIsUserInitiatedChange(state.isUserInitiatedChange()); + builder.setBrightnessMaxReason(state.getBrightnessMaxReason()); return builder; } @@ -506,5 +520,21 @@ public final class DisplayBrightnessState { mIsUserInitiatedChange = isUserInitiatedChange; return this; } + + /** + * Gets reason for max brightness restriction + */ + public @BrightnessInfo.BrightnessMaxReason int getBrightnessMaxReason() { + return mBrightnessMaxReason; + } + + /** + * Sets reason for max brightness restriction + */ + public Builder setBrightnessMaxReason( + @BrightnessInfo.BrightnessMaxReason int brightnessMaxReason) { + mBrightnessMaxReason = brightnessMaxReason; + return this; + } } } diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index cc115f13f5e3..d78fdfae266f 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -150,7 +150,9 @@ import javax.xml.datatype.DatatypeConfigurationException; * <screenBrightnessDefault>0.65</screenBrightnessDefault> * <powerThrottlingConfig> * <brightnessLowestCapAllowed>0.1</brightnessLowestCapAllowed> - * <pollingWindowMillis>15</pollingWindowMillis> + * <customAnimationRateSec>0.004</customAnimationRateSec> + * <pollingWindowMaxMillis>30000</pollingWindowMaxMillis> + * <pollingWindowMinMillis>10000</pollingWindowMinMillis> * <powerThrottlingMap> * <powerThrottlingPoint> * <thermalStatus>severe</thermalStatus> @@ -2184,9 +2186,13 @@ public class DisplayDeviceConfig { return; } float lowestBrightnessCap = powerThrottlingCfg.getBrightnessLowestCapAllowed().floatValue(); - int pollingWindowMillis = powerThrottlingCfg.getPollingWindowMillis().intValue(); + float customAnimationRateSec = powerThrottlingCfg.getCustomAnimationRateSec().floatValue(); + int pollingWindowMaxMillis = powerThrottlingCfg.getPollingWindowMaxMillis().intValue(); + int pollingWindowMinMillis = powerThrottlingCfg.getPollingWindowMinMillis().intValue(); mPowerThrottlingConfigData = new PowerThrottlingConfigData(lowestBrightnessCap, - pollingWindowMillis); + customAnimationRateSec, + pollingWindowMaxMillis, + pollingWindowMinMillis); } private void loadRefreshRateSetting(DisplayConfiguration config) { @@ -2980,12 +2986,19 @@ public class DisplayDeviceConfig { public static class PowerThrottlingConfigData { /** Lowest brightness cap allowed for this device. */ public final float brightnessLowestCapAllowed; - /** Time window for polling power in seconds. */ - public final int pollingWindowMillis; + /** Time take to animate brightness in seconds. */ + public final float customAnimationRateSec; + /** Time window for maximum polling power in milliseconds. */ + public final int pollingWindowMaxMillis; + /** Time window for minimum polling power in milliseconds. */ + public final int pollingWindowMinMillis; public PowerThrottlingConfigData(float brightnessLowestCapAllowed, - int pollingWindowMillis) { + float customAnimationRateSec, int pollingWindowMaxMillis, + int pollingWindowMinMillis) { this.brightnessLowestCapAllowed = brightnessLowestCapAllowed; - this.pollingWindowMillis = pollingWindowMillis; + this.customAnimationRateSec = customAnimationRateSec; + this.pollingWindowMaxMillis = pollingWindowMaxMillis; + this.pollingWindowMinMillis = pollingWindowMinMillis; } @Override @@ -2993,7 +3006,9 @@ public class DisplayDeviceConfig { return "PowerThrottlingConfigData{" + "brightnessLowestCapAllowed: " + brightnessLowestCapAllowed - + ", pollingWindowMillis: " + pollingWindowMillis + + ", customAnimationRateSec: " + customAnimationRateSec + + ", pollingWindowMaxMillis: " + pollingWindowMaxMillis + + ", pollingWindowMinMillis: " + pollingWindowMinMillis + "} "; } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 187caba4db03..99ad65d14ff2 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -3340,6 +3340,7 @@ public final class DisplayManagerService extends SystemService { pw.println(); final int displayStateCount = mDisplayStates.size(); pw.println("Display States: size=" + displayStateCount); + pw.println("---------------------"); for (int i = 0; i < displayStateCount; i++) { final int displayId = mDisplayStates.keyAt(i); final int displayState = mDisplayStates.valueAt(i); @@ -3355,6 +3356,7 @@ public final class DisplayManagerService extends SystemService { pw.println(); pw.println("Display Adapters: size=" + mDisplayAdapters.size()); + pw.println("------------------------"); for (DisplayAdapter adapter : mDisplayAdapters) { pw.println(" " + adapter.getName()); adapter.dumpLocked(ipw); @@ -3362,6 +3364,7 @@ public final class DisplayManagerService extends SystemService { pw.println(); pw.println("Display Devices: size=" + mDisplayDeviceRepo.sizeLocked()); + pw.println("-----------------------"); mDisplayDeviceRepo.forEachLocked(device -> { pw.println(" " + device.getDisplayDeviceInfoLocked()); device.dumpLocked(ipw); @@ -3373,6 +3376,7 @@ public final class DisplayManagerService extends SystemService { final int callbackCount = mCallbacks.size(); pw.println(); pw.println("Callbacks: size=" + callbackCount); + pw.println("-----------------"); for (int i = 0; i < callbackCount; i++) { CallbackRecord callback = mCallbacks.valueAt(i); pw.println(" " + i + ": mPid=" + callback.mPid @@ -3385,6 +3389,7 @@ public final class DisplayManagerService extends SystemService { for (int i = 0; i < displayPowerControllerCount; i++) { mDisplayPowerControllers.valueAt(i).dump(pw); } + pw.println(); mPersistentDataStore.dump(pw); @@ -3403,8 +3408,10 @@ public final class DisplayManagerService extends SystemService { } pw.println(); mDisplayModeDirector.dump(pw); + pw.println(); mBrightnessSynchronizer.dump(pw); if (mSmallAreaDetectionController != null) { + pw.println(); mSmallAreaDetectionController.dump(pw); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index a887f6d241e6..bf559c10b0ba 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -253,10 +253,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // The display blanker. private final DisplayBlanker mBlanker; - // The LogicalDisplay tied to this DisplayPowerController2. + // The LogicalDisplay tied to this DisplayPowerController. private final LogicalDisplay mLogicalDisplay; - // The ID of the LogicalDisplay tied to this DisplayPowerController2. + // The ID of the LogicalDisplay tied to this DisplayPowerController. private final int mDisplayId; // The ID of the display which this display follows for brightness purposes. @@ -466,7 +466,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private ObjectAnimator mColorFadeOffAnimator; private DualRampAnimator<DisplayPowerState> mScreenBrightnessRampAnimator; - // True if this DisplayPowerController2 has been stopped and should no longer be running. + // True if this DisplayPowerController has been stopped and should no longer be running. private boolean mStopped; private DisplayDeviceConfig mDisplayDeviceConfig; @@ -591,7 +591,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mThermalBrightnessThrottlingDataId, logicalDisplay.getPowerThrottlingDataIdLocked(), mDisplayDeviceConfig, displayDeviceInfo.width, displayDeviceInfo.height, - displayToken, mDisplayId), mContext, flags, mSensorManager); + displayToken, mDisplayId), mContext, flags, mSensorManager, + mDisplayBrightnessController.getCurrentBrightness()); // Seed the cached brightness saveBrightnessInfo(getScreenBrightnessSetting()); mAutomaticBrightnessStrategy = @@ -861,7 +862,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mLeadDisplayId = leadDisplayId; final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); if (device == null) { - Slog.wtf(mTag, "Display Device is null in DisplayPowerController2 for display: " + Slog.wtf(mTag, "Display Device is null in DisplayPowerController for display: " + mLogicalDisplay.getDisplayIdLocked()); return; } @@ -935,7 +936,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call /** * Unregisters all listeners and interrupts all running threads; halting future work. * - * This method should be called when the DisplayPowerController2 is no longer in use; i.e. when + * This method should be called when the DisplayPowerController is no longer in use; i.e. when * the {@link #mDisplayId display} has been removed. */ @Override @@ -1588,7 +1589,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // brightness sources (such as an app override) are not saved to the setting, but should be // reflected in HBM calculations. mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState, - mBrightnessClamperController.getBrightnessMaxReason()); + clampedState.getBrightnessMaxReason()); // Animate the screen brightness when the screen is on or dozing. // Skip the animation when the screen is off or suspended. @@ -1804,7 +1805,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (userSetBrightnessChanged || newEvent.getReason().getReason() != BrightnessReason.REASON_TEMPORARY) { - logBrightnessEvent(newEvent, unthrottledBrightnessState); + logBrightnessEvent(newEvent, unthrottledBrightnessState, clampedState); } if (mBrightnessEventRingBuffer != null) { mBrightnessEventRingBuffer.append(newEvent); @@ -1997,6 +1998,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call synchronized (mCachedBrightnessInfo) { float stateMax = state != null ? state.getMaxBrightness() : PowerManager.BRIGHTNESS_MAX; float stateMin = state != null ? state.getMinBrightness() : PowerManager.BRIGHTNESS_MAX; + @BrightnessInfo.BrightnessMaxReason int maxReason = + state != null ? state.getBrightnessMaxReason() + : BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; final float minBrightness = Math.max(stateMin, Math.min( mBrightnessRangeController.getCurrentBrightnessMin(), stateMax)); final float maxBrightness = Math.min( @@ -2023,7 +2027,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRangeController.getTransitionPoint()); changed |= mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason, - mBrightnessClamperController.getBrightnessMaxReason()); + maxReason); return changed; } } @@ -2625,6 +2629,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call synchronized (mLock) { pw.println(); pw.println("Display Power Controller:"); + pw.println("-------------------------"); + pw.println(" mDisplayId=" + mDisplayId); pw.println(" mLeadDisplayId=" + mLeadDisplayId); pw.println(" mLightSensor=" + mLightSensor); @@ -2699,25 +2705,39 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call + mColorFadeOffAnimator.isStarted()); } + pw.println(); if (mPowerState != null) { mPowerState.dump(pw); } + pw.println(); + if (mDisplayBrightnessController != null) { + mDisplayBrightnessController.dump(pw); + } + + pw.println(); + if (mBrightnessClamperController != null) { + mBrightnessClamperController.dump(ipw); + } + + pw.println(); + if (mBrightnessRangeController != null) { + mBrightnessRangeController.dump(pw); + } + + pw.println(); if (mAutomaticBrightnessController != null) { mAutomaticBrightnessController.dump(pw); dumpBrightnessEvents(pw); } - dumpRbcEvents(pw); + pw.println(); if (mScreenOffBrightnessSensorController != null) { mScreenOffBrightnessSensorController.dump(pw); } - if (mBrightnessRangeController != null) { - mBrightnessRangeController.dump(pw); - } - + pw.println(); if (mBrightnessThrottler != null) { mBrightnessThrottler.dump(pw); } @@ -2729,24 +2749,13 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } pw.println(); - if (mWakelockController != null) { mWakelockController.dumpLocal(pw); } pw.println(); - if (mDisplayBrightnessController != null) { - mDisplayBrightnessController.dump(pw); - } - - pw.println(); if (mDisplayStateController != null) { - mDisplayStateController.dumpsys(pw); - } - - pw.println(); - if (mBrightnessClamperController != null) { - mBrightnessClamperController.dump(ipw); + mDisplayStateController.dump(pw); } } @@ -2926,7 +2935,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call return FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__ENTIRE_REASON__REASON_UNKNOWN; } - private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness) { + private void logBrightnessEvent(BrightnessEvent event, float unmodifiedBrightness, + DisplayBrightnessState brightnessState) { int modifier = event.getReason().getModifier(); int flags = event.getFlags(); // It's easier to check if the brightness is at maximum level using the brightness @@ -2963,7 +2973,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT, event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, (modifier & BrightnessReason.MODIFIER_LOW_POWER) > 0, - mBrightnessClamperController.getBrightnessMaxReason(), + brightnessState.getBrightnessMaxReason(), // TODO: (flc) add brightnessMinReason here too. (modifier & BrightnessReason.MODIFIER_DIMMED) > 0, event.isRbcEnabled(), @@ -3296,10 +3306,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call BrightnessClamperController getBrightnessClamperController(Handler handler, BrightnessClamperController.ClamperChangeListener clamperChangeListener, BrightnessClamperController.DisplayDeviceData data, Context context, - DisplayManagerFlags flags, SensorManager sensorManager) { + DisplayManagerFlags flags, SensorManager sensorManager, float currentBrightness) { return new BrightnessClamperController(handler, clamperChangeListener, data, context, - flags, sensorManager); + flags, sensorManager, currentBrightness); } DisplayWhiteBalanceController getDisplayWhiteBalanceController(Handler handler, diff --git a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java index 882c02faedf9..215932ca19be 100644 --- a/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java +++ b/services/core/java/com/android/server/display/DisplayPowerProximityStateController.java @@ -315,6 +315,7 @@ public final class DisplayPowerProximityStateController { public void dumpLocal(PrintWriter pw) { pw.println(); pw.println("DisplayPowerProximityStateController:"); + pw.println("-------------------------------------"); synchronized (mLock) { pw.println(" mPendingWaitForNegativeProximityLocked=" + mPendingWaitForNegativeProximityLocked); diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java index e5efebcf1b41..2fbb114c9a63 100644 --- a/services/core/java/com/android/server/display/DisplayPowerState.java +++ b/services/core/java/com/android/server/display/DisplayPowerState.java @@ -344,7 +344,6 @@ final class DisplayPowerState { } public void dump(PrintWriter pw) { - pw.println(); pw.println("Display Power State:"); pw.println(" mStopped=" + mStopped); pw.println(" mScreenState=" + Display.stateToString(mScreenState)); diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 6e0b9cf2f234..0570b2ab510b 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -1286,7 +1286,10 @@ final class LocalDisplayAdapter extends DisplayAdapter { pw.println(" " + mSupportedModes.valueAt(i)); } pw.println("mSupportedColorModes=" + mSupportedColorModes); - pw.println("mDisplayDeviceConfig=" + mDisplayDeviceConfig); + pw.println(""); + pw.println("DisplayDeviceConfig: "); + pw.println("---------------------"); + pw.println(mDisplayDeviceConfig); } private int findSfDisplayModeIdLocked(int displayModeId, int modeGroup) { diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index 5d55d1904f1b..e8be8a449652 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -1040,6 +1040,9 @@ final class LogicalDisplay { public void dumpLocked(PrintWriter pw) { pw.println("mDisplayId=" + mDisplayId); + pw.println("mPrimaryDisplayDevice=" + (mPrimaryDisplayDevice != null + ? mPrimaryDisplayDevice.getNameLocked() + "(" + mPrimaryDisplayDevice.getUniqueId() + + ")" : "null")); pw.println("mIsEnabled=" + mIsEnabled); pw.println("mIsInTransition=" + mIsInTransition); pw.println("mLayerStack=" + mLayerStack); @@ -1049,8 +1052,6 @@ final class LogicalDisplay { pw.println("mRequestedColorMode=" + mRequestedColorMode); pw.println("mDisplayOffset=(" + mDisplayOffsetX + ", " + mDisplayOffsetY + ")"); pw.println("mDisplayScalingDisabled=" + mDisplayScalingDisabled); - pw.println("mPrimaryDisplayDevice=" + (mPrimaryDisplayDevice != null ? - mPrimaryDisplayDevice.getNameLocked() : "null")); pw.println("mBaseDisplayInfo=" + mBaseDisplayInfo); pw.println("mOverrideDisplayInfo=" + mOverrideDisplayInfo); pw.println("mRequestedMinimalPostProcessing=" + mRequestedMinimalPostProcessing); diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index e9ecfc67b7db..c3f6a5285ae3 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -427,6 +427,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { public void dumpLocked(PrintWriter pw) { pw.println("LogicalDisplayMapper:"); + pw.println("---------------------"); IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); ipw.increaseIndent(); @@ -477,14 +478,14 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // The boot animation might still be in progress, we do not want to switch states now // as the boot animation would end up with an incorrect size. if (DEBUG) { - Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState - + " until boot is completed"); + Slog.d(TAG, "Postponing transition to state: " + + mPendingDeviceState.getIdentifier() + " until boot is completed"); } mDeviceStateToBeAppliedAfterBoot = state; return; } - Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + Slog.i(TAG, "Requesting Transition to state: " + state.getIdentifier() + ", from state=" + mDeviceState.getIdentifier() + ", interactive=" + mInteractive + ", mBootCompleted=" + mBootCompleted); // As part of a state transition, we may need to turn off some displays temporarily so that diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java index 2d7792d01c53..9cdc91865bf8 100644 --- a/services/core/java/com/android/server/display/PersistentDataStore.java +++ b/services/core/java/com/android/server/display/PersistentDataStore.java @@ -628,7 +628,9 @@ final class PersistentDataStore { } public void dump(PrintWriter pw) { - pw.println("PersistentDataStore"); + pw.println("PersistentDataStore:"); + pw.println("--------------------"); + pw.println(" mLoaded=" + mLoaded); pw.println(" mDirty=" + mDirty); pw.println(" RememberedWifiDisplays:"); diff --git a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java index 0a884c98402e..b63eba31c948 100644 --- a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java +++ b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java @@ -121,6 +121,7 @@ public class ScreenOffBrightnessSensorController implements SensorEventListener /** Dump current state */ public void dump(PrintWriter pw) { pw.println("Screen Off Brightness Sensor Controller:"); + pw.println("----------------------------------------"); IndentingPrintWriter idpw = new IndentingPrintWriter(pw); idpw.increaseIndent(); idpw.println("registered=" + mRegistered); diff --git a/services/core/java/com/android/server/display/SmallAreaDetectionController.java b/services/core/java/com/android/server/display/SmallAreaDetectionController.java index bf384b02d95e..3ed7e5756b8f 100644 --- a/services/core/java/com/android/server/display/SmallAreaDetectionController.java +++ b/services/core/java/com/android/server/display/SmallAreaDetectionController.java @@ -133,7 +133,8 @@ final class SmallAreaDetectionController { } void dump(PrintWriter pw) { - pw.println("Small area detection allowlist"); + pw.println("Small area detection allowlist:"); + pw.println("-------------------------------"); pw.println(" Packages:"); synchronized (mLock) { for (String pkg : mAllowPkgMap.keySet()) { diff --git a/services/core/java/com/android/server/display/WakelockController.java b/services/core/java/com/android/server/display/WakelockController.java index 5b0229cbb393..35207236d1cf 100644 --- a/services/core/java/com/android/server/display/WakelockController.java +++ b/services/core/java/com/android/server/display/WakelockController.java @@ -417,6 +417,7 @@ public final class WakelockController { */ public void dumpLocal(PrintWriter pw) { pw.println("WakelockController State:"); + pw.println("-------------------------"); pw.println(" mDisplayId=" + mDisplayId); pw.println(" mUnfinishedBusiness=" + hasUnfinishedBusiness()); pw.println(" mOnStateChangePending=" + isOnStateChangedPending()); diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java index 1b49bbc74f92..06890e72f068 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java @@ -262,6 +262,7 @@ public class DisplayBrightnessStrategySelector { public void dump(PrintWriter writer) { writer.println(); writer.println("DisplayBrightnessStrategySelector:"); + writer.println("----------------------------------"); writer.println(" mDisplayId= " + mDisplayId); writer.println(" mOldBrightnessStrategyName= " + mOldBrightnessStrategyName); writer.println( diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index 59fffe7ee393..d3be33f51e5c 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -75,10 +75,13 @@ public class BrightnessClamperController { private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState(); private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener; + private final DisplayManagerFlags mDisplayManagerFlags; private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; private float mCustomAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; @Nullable + private BrightnessPowerClamper mPowerClamper; + @Nullable private Type mClamperType = null; private boolean mClamperApplied = false; @@ -93,16 +96,18 @@ public class BrightnessClamperController { public BrightnessClamperController(Handler handler, ClamperChangeListener clamperChangeListener, DisplayDeviceData data, Context context, - DisplayManagerFlags flags, SensorManager sensorManager) { - this(new Injector(), handler, clamperChangeListener, data, context, flags, sensorManager); + DisplayManagerFlags flags, SensorManager sensorManager, float currentBrightness) { + this(new Injector(), handler, clamperChangeListener, data, context, flags, sensorManager, + currentBrightness); } @VisibleForTesting BrightnessClamperController(Injector injector, Handler handler, ClamperChangeListener clamperChangeListener, DisplayDeviceData data, Context context, - DisplayManagerFlags flags, SensorManager sensorManager) { + DisplayManagerFlags flags, SensorManager sensorManager, float currentBrightness) { mDeviceConfigParameterProvider = injector.getDeviceConfigParameterProvider(); mHandler = handler; + mDisplayManagerFlags = flags; mLightSensorController = injector.getLightSensorController(sensorManager, context, mLightSensorListener, mHandler); @@ -117,7 +122,15 @@ public class BrightnessClamperController { }; mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags, - context); + context, currentBrightness); + if (mDisplayManagerFlags.isPowerThrottlingClamperEnabled()) { + for (BrightnessClamper clamper: mClampers) { + if (clamper.getType() == Type.POWER) { + mPowerClamper = (BrightnessPowerClamper) clamper; + break; + } + } + } mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener, data); @@ -161,6 +174,7 @@ public class BrightnessClamperController { builder.setBrightness(cappedBrightness); builder.setMaxBrightness(mBrightnessCap); builder.setCustomAnimationRate(mCustomAnimationRate); + builder.setBrightnessMaxReason(getBrightnessMaxReason()); if (mClamperType != null) { builder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_THROTTLED); @@ -182,22 +196,17 @@ public class BrightnessClamperController { mModifiers.get(i).apply(request, builder); } + if (mDisplayManagerFlags.isPowerThrottlingClamperEnabled()) { + if (mPowerClamper != null) { + mPowerClamper.updateCurrentBrightness(cappedBrightness); + } + } + return builder.build(); } - /** - * See BrightnessThrottler.getBrightnessMaxReason: - * used in: - * 1) DPC2.CachedBrightnessInfo to determine changes - * 2) DPC2.logBrightnessEvent - * 3) HBMController - for logging - * Method is called in mHandler thread (DisplayControllerHandler), in the same thread - * recalculateBrightnessCap and DPC2.updatePowerStateInternal are called. - * Should be moved to DisplayBrightnessState OR derived from DisplayBrightnessState - * TODO: b/263362199 - */ @BrightnessInfo.BrightnessMaxReason - public int getBrightnessMaxReason() { + private int getBrightnessMaxReason() { if (mClamperType == null) { return BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; } else if (mClamperType == Type.THERMAL) { @@ -224,6 +233,7 @@ public class BrightnessClamperController { */ public void dump(PrintWriter writer) { writer.println("BrightnessClamperController:"); + writer.println("----------------------------"); writer.println(" mBrightnessCap: " + mBrightnessCap); writer.println(" mClamperType: " + mClamperType); writer.println(" mClamperApplied: " + mClamperApplied); @@ -321,13 +331,17 @@ public class BrightnessClamperController { List<BrightnessClamper<? super DisplayDeviceData>> getClampers(Handler handler, ClamperChangeListener clamperChangeListener, DisplayDeviceData data, - DisplayManagerFlags flags, Context context) { + DisplayManagerFlags flags, Context context, float currentBrightness) { List<BrightnessClamper<? super DisplayDeviceData>> clampers = new ArrayList<>(); clampers.add( new BrightnessThermalClamper(handler, clamperChangeListener, data)); if (flags.isPowerThrottlingClamperEnabled()) { - clampers.add(new BrightnessPowerClamper(handler, clamperChangeListener, - data)); + // Check if power-throttling config is present. + PowerThrottlingConfigData configData = data.getPowerThrottlingConfigData(); + if (configData != null) { + clampers.add(new BrightnessPowerClamper(handler, clamperChangeListener, + data, currentBrightness)); + } } if (flags.isBrightnessWearBedtimeModeClamperEnabled()) { clampers.add(new BrightnessWearBedtimeModeClamper(handler, context, diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java index 790322d75251..85e81f989845 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java @@ -21,16 +21,23 @@ import static com.android.server.display.brightness.clamper.BrightnessClamperCon import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.os.Handler; +import android.os.IThermalEventListener; +import android.os.IThermalService; import android.os.PowerManager; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.Temperature; import android.provider.DeviceConfigInterface; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.DisplayBrightnessState; import com.android.server.display.DisplayDeviceConfig.PowerThrottlingConfigData; import com.android.server.display.DisplayDeviceConfig.PowerThrottlingData; import com.android.server.display.DisplayDeviceConfig.PowerThrottlingData.ThrottlingLevel; +import com.android.server.display.brightness.BrightnessUtils; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.utils.DeviceConfigParsingUtils; @@ -65,14 +72,21 @@ class BrightnessPowerClamper extends private PowerThrottlingData mPowerThrottlingDataActive = null; @Nullable private PowerThrottlingConfigData mPowerThrottlingConfigData = null; - + @NonNull + private final ThermalLevelListener mThermalLevelListener; + @NonNull + private final PowerChangeListener mPowerChangeListener; private @Temperature.ThrottlingStatus int mCurrentThermalLevel = Temperature.THROTTLING_NONE; + private boolean mCurrentThermalLevelChanged = false; private float mCurrentAvgPowerConsumed = 0; @Nullable private String mUniqueDisplayId = null; @Nullable private String mDataId = null; - + private float mCurrentBrightness = PowerManager.BRIGHTNESS_INVALID; + private float mCustomAnimationRateSec = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; + private float mCustomAnimationRateSecDeviceConfig = + DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; private final BiFunction<String, String, ThrottlingLevel> mDataPointMapper = (key, value) -> { try { int status = DeviceConfigParsingUtils.parseThermalStatus(key); @@ -88,23 +102,41 @@ class BrightnessPowerClamper extends BrightnessPowerClamper(Handler handler, ClamperChangeListener listener, - PowerData powerData) { - this(new Injector(), handler, listener, powerData); + PowerData powerData, float currentBrightness) { + this(new Injector(), handler, listener, powerData, currentBrightness); } @VisibleForTesting BrightnessPowerClamper(Injector injector, Handler handler, ClamperChangeListener listener, - PowerData powerData) { + PowerData powerData, float currentBrightness) { super(handler, listener); mInjector = injector; - mConfigParameterProvider = injector.getDeviceConfigParameterProvider(); + mCurrentBrightness = currentBrightness; + mPowerChangeListener = (powerConsumed, thermalStatus) -> { + recalculatePowerQuotaChange(powerConsumed, thermalStatus); + }; + mPowerThrottlingConfigData = powerData.getPowerThrottlingConfigData(); + if (mPowerThrottlingConfigData != null) { + mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec; + } + mThermalLevelListener = new ThermalLevelListener(handler); + mPmicMonitor = + mInjector.getPmicMonitor(mPowerChangeListener, + mThermalLevelListener.getThermalService(), + mPowerThrottlingConfigData.pollingWindowMaxMillis, + mPowerThrottlingConfigData.pollingWindowMinMillis); + mConfigParameterProvider = injector.getDeviceConfigParameterProvider(); mHandler.post(() -> { setDisplayData(powerData); loadOverrideData(); start(); }); + } + @VisibleForTesting + PowerChangeListener getPowerChangeListener() { + return mPowerChangeListener; } @Override @@ -114,6 +146,11 @@ class BrightnessPowerClamper extends } @Override + float getCustomAnimationRate() { + return mCustomAnimationRateSec; + } + + @Override void onDeviceConfigChanged() { mHandler.post(() -> { loadOverrideData(); @@ -134,6 +171,9 @@ class BrightnessPowerClamper extends if (mPmicMonitor != null) { mPmicMonitor.shutdown(); } + if (mThermalLevelListener != null) { + mThermalLevelListener.stop(); + } } /** @@ -144,11 +184,20 @@ class BrightnessPowerClamper extends pw.println(" mCurrentAvgPowerConsumed=" + mCurrentAvgPowerConsumed); pw.println(" mUniqueDisplayId=" + mUniqueDisplayId); pw.println(" mCurrentThermalLevel=" + mCurrentThermalLevel); + pw.println(" mCurrentThermalLevelChanged=" + mCurrentThermalLevelChanged); pw.println(" mPowerThrottlingDataFromDDC=" + (mPowerThrottlingDataFromDDC == null ? "null" : mPowerThrottlingDataFromDDC.toString())); + mThermalLevelListener.dump(pw); super.dump(pw); } + /** + * Updates current brightness, for power calculations. + */ + public void updateCurrentBrightness(float currentBrightness) { + mCurrentBrightness = currentBrightness; + } + private void recalculateActiveData() { if (mUniqueDisplayId == null || mDataId == null) { return; @@ -156,17 +205,11 @@ class BrightnessPowerClamper extends mPowerThrottlingDataActive = mPowerThrottlingDataOverride .getOrDefault(mUniqueDisplayId, Map.of()).getOrDefault(mDataId, mPowerThrottlingDataFromDDC); - if (mPowerThrottlingDataActive != null) { - if (mPmicMonitor != null) { - mPmicMonitor.stop(); - mPmicMonitor.start(); - } - } else { + if (mPowerThrottlingDataActive == null) { if (mPmicMonitor != null) { mPmicMonitor.stop(); } } - recalculateBrightnessCap(); } private void loadOverrideData() { @@ -198,21 +241,57 @@ class BrightnessPowerClamper extends if (mPowerThrottlingDataActive == null) { return; } - if (powerQuota > 0 && mCurrentAvgPowerConsumed > powerQuota) { - isActive = true; - // calculate new brightness Cap. - // Brightness has a linear relation to power-consumed. - targetBrightnessCap = - (powerQuota / mCurrentAvgPowerConsumed) * PowerManager.BRIGHTNESS_MAX; - // Cap to lowest allowed brightness on device. + if (powerQuota > 0) { + if (BrightnessUtils.isValidBrightnessValue(mCurrentBrightness) + && (mCurrentAvgPowerConsumed > powerQuota)) { + isActive = true; + // calculate new brightness Cap. + // Brightness has a linear relation to power-consumed. + targetBrightnessCap = + (powerQuota / mCurrentAvgPowerConsumed) * mCurrentBrightness; + } else if (mCurrentThermalLevelChanged) { + if (mCurrentThermalLevel == Temperature.THROTTLING_NONE) { + // reset pmic and remove the power-throttling cap. + isActive = true; + targetBrightnessCap = PowerManager.BRIGHTNESS_MAX; + mPmicMonitor.stop(); + } else { + isActive = true; + // Since the thermal status has changed, we need to remove power-throttling cap. + // Instead of recalculating and changing brightness again, adding flicker, + // we will wait for the next pmic cycle to re-evaluate this value + // make act on it, if needed. + targetBrightnessCap = PowerManager.BRIGHTNESS_MAX; + if (mPmicMonitor.isStopped()) { + mPmicMonitor.start(); + } + } + } else { // Current power consumed is under the quota. + isActive = true; + targetBrightnessCap = PowerManager.BRIGHTNESS_MAX; + } + } + + // Cap to lowest allowed brightness on device. + if (mPowerThrottlingConfigData != null) { targetBrightnessCap = Math.max(targetBrightnessCap, - mPowerThrottlingConfigData.brightnessLowestCapAllowed); + mPowerThrottlingConfigData.brightnessLowestCapAllowed); } if (mBrightnessCap != targetBrightnessCap || mIsActive != isActive) { mIsActive = isActive; + Slog.i(TAG, "Power clamper changing current brightness cap mBrightnessCap: " + + mBrightnessCap + " to target brightness cap:" + targetBrightnessCap + + " for current screen brightness: " + mCurrentBrightness); mBrightnessCap = targetBrightnessCap; + Slog.i(TAG, "Power clamper changed state: thermalStatus:" + mCurrentThermalLevel + + " mCurrentThermalLevelChanged:" + mCurrentThermalLevelChanged + + " mCurrentAvgPowerConsumed:" + mCurrentAvgPowerConsumed + + " mCustomAnimationRateSec:" + mCustomAnimationRateSecDeviceConfig); + mCustomAnimationRateSec = mCustomAnimationRateSecDeviceConfig; mChangeListener.onChanged(); + } else { + mCustomAnimationRateSec = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; } } @@ -234,6 +313,11 @@ class BrightnessPowerClamper extends private void recalculatePowerQuotaChange(float avgPowerConsumed, int thermalStatus) { mHandler.post(() -> { + if (mCurrentThermalLevel != thermalStatus) { + mCurrentThermalLevelChanged = true; + } else { + mCurrentThermalLevelChanged = false; + } mCurrentThermalLevel = thermalStatus; mCurrentAvgPowerConsumed = avgPowerConsumed; recalculateBrightnessCap(); @@ -244,14 +328,107 @@ class BrightnessPowerClamper extends if (mPowerThrottlingConfigData == null) { return; } - PowerChangeListener listener = (powerConsumed, thermalStatus) -> { - recalculatePowerQuotaChange(powerConsumed, thermalStatus); - }; - mPmicMonitor = - mInjector.getPmicMonitor(listener, mPowerThrottlingConfigData.pollingWindowMillis); + if (mPowerThrottlingConfigData.pollingWindowMaxMillis + <= mPowerThrottlingConfigData.pollingWindowMinMillis) { + Slog.e(TAG, "Brightness power max polling window:" + + mPowerThrottlingConfigData.pollingWindowMaxMillis + + " msec, should be greater than brightness min polling window:" + + mPowerThrottlingConfigData.pollingWindowMinMillis + " msec."); + return; + } + if ((mPowerThrottlingConfigData.pollingWindowMaxMillis + % mPowerThrottlingConfigData.pollingWindowMinMillis) != 0) { + Slog.e(TAG, "Brightness power max polling window:" + + mPowerThrottlingConfigData.pollingWindowMaxMillis + + " msec, is not divisible by brightness min polling window:" + + mPowerThrottlingConfigData.pollingWindowMinMillis + " msec."); + return; + } + mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec; + mThermalLevelListener.start(); + } + + private void activatePmicMonitor() { + if (!mPmicMonitor.isStopped()) { + return; + } mPmicMonitor.start(); } + private void deactivatePmicMonitor(@Temperature.ThrottlingStatus int status) { + if (status != Temperature.THROTTLING_NONE) { + return; + } + if (mPmicMonitor.isStopped()) { + return; + } + mPmicMonitor.stop(); + } + + private final class ThermalLevelListener extends IThermalEventListener.Stub { + private final Handler mHandler; + private IThermalService mThermalService; + private boolean mStarted; + + ThermalLevelListener(Handler handler) { + mHandler = handler; + mStarted = false; + mThermalService = IThermalService.Stub.asInterface( + ServiceManager.getService(Context.THERMAL_SERVICE)); + } + + IThermalService getThermalService() { + return mThermalService; + } + + void start() { + if (mStarted) { + return; + } + if (mThermalService == null) { + return; + } + try { + // TODO b/279114539 Try DISPLAY first and then fallback to SKIN. + mThermalService.registerThermalEventListenerWithType(this, Temperature.TYPE_SKIN); + mStarted = true; + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register thermal status listener", e); + } + } + + @Override + public void notifyThrottling(Temperature temp) { + @Temperature.ThrottlingStatus int status = temp.getStatus(); + if (status >= Temperature.THROTTLING_LIGHT) { + Slog.d(TAG, "Activating pmic monitor due to thermal state:" + status); + mHandler.post(() -> activatePmicMonitor()); + } else { + if (!mPmicMonitor.isStopped()) { + mHandler.post(() -> deactivatePmicMonitor(status)); + } + } + } + + void stop() { + if (!mStarted) { + return; + } + try { + mThermalService.unregisterThermalEventListener(this); + mStarted = false; + } catch (RemoteException e) { + Slog.e(TAG, "Failed to unregister thermal status listener", e); + } + mThermalService = null; + } + + void dump(PrintWriter writer) { + writer.println(" ThermalLevelObserver:"); + writer.println(" mStarted: " + mStarted); + } + } + public interface PowerData { @NonNull String getUniqueDisplayId(); @@ -279,8 +456,12 @@ class BrightnessPowerClamper extends @VisibleForTesting static class Injector { - PmicMonitor getPmicMonitor(PowerChangeListener listener, int pollingTime) { - return new PmicMonitor(listener, pollingTime); + PmicMonitor getPmicMonitor(PowerChangeListener powerChangeListener, + IThermalService thermalService, + int pollingMaxTimeMillis, + int pollingMinTimeMillis) { + return new PmicMonitor(powerChangeListener, thermalService, pollingMaxTimeMillis, + pollingMinTimeMillis); } DeviceConfigParameterProvider getDeviceConfigParameterProvider() { diff --git a/services/core/java/com/android/server/display/brightness/clamper/PmicMonitor.java b/services/core/java/com/android/server/display/brightness/clamper/PmicMonitor.java index 26784f2353ff..355f4fe65279 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/PmicMonitor.java +++ b/services/core/java/com/android/server/display/brightness/clamper/PmicMonitor.java @@ -18,15 +18,12 @@ package com.android.server.display.brightness.clamper; import static com.android.server.display.brightness.clamper.BrightnessPowerClamper.PowerChangeListener; -import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.Context; import android.hardware.power.stats.EnergyConsumer; import android.hardware.power.stats.EnergyConsumerResult; import android.hardware.power.stats.EnergyConsumerType; import android.os.IThermalService; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.Temperature; import android.power.PowerStatsInternal; import android.util.IntArray; @@ -51,25 +48,30 @@ public class PmicMonitor { // The executor to periodically monitor the display power. private final ScheduledExecutorService mExecutor; - @NonNull - private final PowerChangeListener mPowerChangeListener; - private final long mPowerMonitorPeriodConfigSecs; + private final long mPowerMonitorPeriodConfigMillis; private final PowerStatsInternal mPowerStatsInternal; @VisibleForTesting final IThermalService mThermalService; + @VisibleForTesting PowerChangeListener mPowerChangeListener; private ScheduledFuture<?> mPmicMonitorFuture; private float mLastEnergyConsumed = 0; - private float mCurrentAvgPower = 0; + private float mCurrentTotalAvgPower = 0; private Temperature mCurrentTemperature; private long mCurrentTimestampMillis = 0; - - PmicMonitor(PowerChangeListener listener, int powerMonitorPeriodConfigSecs) { + private float[] mAvgPowerCircularList; + private int mPowerListStart = 0; + private int mPowerListEnd = 0; + + PmicMonitor(PowerChangeListener listener, + IThermalService thermalService, + int pollingMaxTimeMillis, + int pollingMinTimeMillis) { mPowerChangeListener = listener; + mThermalService = thermalService; mPowerStatsInternal = LocalServices.getService(PowerStatsInternal.class); - mThermalService = IThermalService.Stub.asInterface( - ServiceManager.getService(Context.THERMAL_SERVICE)); + mAvgPowerCircularList = new float[pollingMaxTimeMillis / pollingMinTimeMillis]; // start a periodic worker thread. mExecutor = Executors.newSingleThreadScheduledExecutor(); - mPowerMonitorPeriodConfigSecs = (long) powerMonitorPeriodConfigSecs; + mPowerMonitorPeriodConfigMillis = pollingMinTimeMillis; } @Nullable @@ -141,12 +143,28 @@ public class PmicMonitor { // capture thermal state. Temperature displayTemperature = getDisplayTemperature(); - mCurrentAvgPower = currentPower; + boolean isBufferFull = false; + mAvgPowerCircularList[mPowerListEnd] = currentPower; + mCurrentTotalAvgPower += currentPower; + mPowerListEnd = + (mPowerListEnd + 1) % mAvgPowerCircularList.length; + if (mPowerListStart == mPowerListEnd) { + isBufferFull = true; + } + mCurrentTemperature = displayTemperature; mLastEnergyConsumed = displayResults[0].energyUWs; mCurrentTimestampMillis = displayResults[0].timestampMs; - if (mCurrentTemperature != null) { - mPowerChangeListener.onChanged(mCurrentAvgPower, mCurrentTemperature.getStatus()); + + if (mCurrentTemperature != null && isBufferFull) { + mPowerChangeListener.onChanged(mCurrentTotalAvgPower / mAvgPowerCircularList.length, + mCurrentTemperature.getStatus()); + } + + // average power long-term list is full, reset values for next cycle. + if (isBufferFull) { + mCurrentTotalAvgPower = mCurrentTotalAvgPower - mAvgPowerCircularList[mPowerListStart]; + mPowerListStart = (mPowerListStart + 1) % mAvgPowerCircularList.length; } } @@ -165,11 +183,11 @@ public class PmicMonitor { if (mPmicMonitorFuture == null) { mPmicMonitorFuture = mExecutor.scheduleAtFixedRate( this::capturePeriodicDisplayPower, - mPowerMonitorPeriodConfigSecs, - mPowerMonitorPeriodConfigSecs, - TimeUnit.SECONDS); + mPowerMonitorPeriodConfigMillis, + mPowerMonitorPeriodConfigMillis, + TimeUnit.MILLISECONDS); } else { - Slog.e(TAG, "already scheduled, stop() called before start."); + Slog.e(TAG, "PMIC already scheduled, stop() called before start."); } } @@ -184,6 +202,23 @@ public class PmicMonitor { } /** + * Updates PMIC configuration. + */ + public void updateConfiguration() { + if (mPmicMonitorFuture != null) { + mPmicMonitorFuture.cancel(true); + mPmicMonitorFuture = null; + } + } + + /** + * Returns if PMIC monitor is stopped. + */ + public boolean isStopped() { + return mPmicMonitorFuture == null; + } + + /** * Shutdown power IC service and worker thread. */ public void shutdown() { diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategy.java index 1db9bbe27ecc..91c985830b97 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategy.java @@ -100,6 +100,7 @@ public final class AutoBrightnessFallbackStrategy implements DisplayBrightnessSt writer.println("AutoBrightnessFallbackStrategy:"); writer.println(" mLeadDisplayId=" + mLeadDisplayId); writer.println(" mIsDisplayEnabled=" + mIsDisplayEnabled); + writer.println(""); if (mScreenOffBrightnessSensorController != null) { IndentingPrintWriter ipw = new IndentingPrintWriter(writer, " "); mScreenOffBrightnessSensorController.dump(ipw); diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index 35be0f3bc942..69b67c87afb9 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -420,6 +420,7 @@ public class DisplayManagerFlags { */ public void dump(PrintWriter pw) { pw.println("DisplayManagerFlags:"); + pw.println("--------------------"); pw.println(" " + mAdaptiveToneImprovements1); pw.println(" " + mAdaptiveToneImprovements2); pw.println(" " + mBackUpSmoothDisplayAndForcePeakRefreshRateFlagState); diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index d909004e6381..18e0d6ee5ea3 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -578,7 +578,8 @@ public class DisplayModeDirector { * @param pw The stream to dump information to. */ public void dump(PrintWriter pw) { - pw.println("DisplayModeDirector"); + pw.println("DisplayModeDirector:"); + pw.println("--------------------"); synchronized (mLock) { pw.println(" mSupportedModesByDisplay:"); for (int i = 0; i < mSupportedModesByDisplay.size(); i++) { diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java index 21bb208981c8..dba687413496 100644 --- a/services/core/java/com/android/server/display/state/DisplayStateController.java +++ b/services/core/java/com/android/server/display/state/DisplayStateController.java @@ -114,9 +114,9 @@ public class DisplayStateController { * * @param pw The PrintWriter used to dump the state. */ - public void dumpsys(PrintWriter pw) { - pw.println(); + public void dump(PrintWriter pw) { pw.println("DisplayStateController:"); + pw.println("-----------------------"); pw.println(" mPerformScreenOffTransition:" + mPerformScreenOffTransition); pw.println(" mDozeStateOverride=" + mDozeStateOverride); diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java index 7ea576d1ed3a..49c45a774dcf 100644 --- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java +++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java @@ -427,7 +427,8 @@ public class DisplayWhiteBalanceController implements * The writer used to dump the state. */ public void dump(PrintWriter writer) { - writer.println("DisplayWhiteBalanceController"); + writer.println("DisplayWhiteBalanceController:"); + writer.println("------------------------------"); writer.println(" mLoggingEnabled=" + mLoggingEnabled); writer.println(" mEnabled=" + mEnabled); writer.println(" mStrongModeEnabled=" + mStrongModeEnabled); diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java index 0efb7494c5d0..a5755ac78a7f 100644 --- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java +++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceSettings.java @@ -23,7 +23,6 @@ import android.os.Looper; import android.os.Message; import android.util.Slog; -import com.android.internal.util.Preconditions; import com.android.server.LocalServices; import com.android.server.display.color.ColorDisplayService; import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal; @@ -129,7 +128,8 @@ public class DisplayWhiteBalanceSettings implements * The writer used to dump the state. */ public void dump(PrintWriter writer) { - writer.println("DisplayWhiteBalanceSettings"); + writer.println("DisplayWhiteBalanceSettings:"); + writer.println("----------------------------"); writer.println(" mLoggingEnabled=" + mLoggingEnabled); writer.println(" mContext=" + mContext); writer.println(" mHandler=" + mHandler); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 6ff0e04bca77..333b3e25cbfa 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -226,10 +226,8 @@ public class ZenModeHelper { mDefaultConfig = Flags.modesUi() ? ZenModeConfig.getDefaultConfig() : readDefaultConfig(mContext.getResources()); - updateDefaultConfigAutomaticRules(); - if (Flags.modesApi()) { - updateDefaultAutomaticRulePolicies(); - } + updateDefaultConfig(mContext, mDefaultConfig); + mConfig = mDefaultConfig.copy(); synchronized (mConfigsArrayLock) { mConfigs.put(UserHandle.USER_SYSTEM, mConfig); @@ -1073,7 +1071,7 @@ public class ZenModeHelper { } void updateZenRulesOnLocaleChange() { - updateDefaultConfigAutomaticRules(); + updateRuleStringsForCurrentLocale(mContext, mDefaultConfig); synchronized (mConfigLock) { if (mConfig == null) { return; @@ -2229,30 +2227,49 @@ public class ZenModeHelper { } } - private void updateDefaultConfigAutomaticRules() { - for (ZenRule rule : mDefaultConfig.automaticRules.values()) { + /** + * Apply changes to the <em>default</em> {@link ZenModeConfig} so that the rules included by + * default (Events / Sleeping) support the latest Zen features and are ready for new users. + * + * <p>This includes: setting a fully populated ZenPolicy, setting correct type and + * allowManualInvocation=true, and ensuring default names and trigger descriptions correspond + * to the current locale. + */ + private static void updateDefaultConfig(Context context, ZenModeConfig defaultConfig) { + if (Flags.modesApi()) { + updateDefaultAutomaticRulePolicies(defaultConfig); + } + if (Flags.modesApi() && Flags.modesUi()) { + SystemZenRules.maybeUpgradeRules(context, defaultConfig); + } + updateRuleStringsForCurrentLocale(context, defaultConfig); + } + + private static void updateRuleStringsForCurrentLocale(Context context, + ZenModeConfig defaultConfig) { + for (ZenRule rule : defaultConfig.automaticRules.values()) { if (ZenModeConfig.EVENTS_DEFAULT_RULE_ID.equals(rule.id)) { - rule.name = mContext.getResources() + rule.name = context.getResources() .getString(R.string.zen_mode_default_events_name); } else if (ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID.equals(rule.id)) { - rule.name = mContext.getResources() + rule.name = context.getResources() .getString(R.string.zen_mode_default_every_night_name); } if (Flags.modesApi() && Flags.modesUi()) { - SystemZenRules.updateTriggerDescription(mContext, rule); + SystemZenRules.updateTriggerDescription(context, rule); } } } // Updates the policies in the default automatic rules (provided via default XML config) to // be fully filled in default values. - private void updateDefaultAutomaticRulePolicies() { + private static void updateDefaultAutomaticRulePolicies(ZenModeConfig defaultConfig) { if (!Flags.modesApi()) { // Should be checked before calling, but just in case. return; } - ZenPolicy defaultPolicy = mDefaultConfig.getZenPolicy(); - for (ZenRule rule : mDefaultConfig.automaticRules.values()) { + ZenPolicy defaultPolicy = defaultConfig.getZenPolicy(); + for (ZenRule rule : defaultConfig.automaticRules.values()) { if (ZenModeConfig.DEFAULT_RULE_IDS.contains(rule.id) && rule.zenPolicy == null) { rule.zenPolicy = defaultPolicy.copy(); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2124ff6b07e0..ff9c3e5f467b 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -60,6 +60,7 @@ import android.app.ApplicationExitInfo; import android.app.ApplicationPackageManager; import android.app.BroadcastOptions; import android.app.IActivityManager; +import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.IDevicePolicyManager; import android.app.admin.SecurityLog; import android.app.backup.IBackupManager; @@ -3372,8 +3373,10 @@ public class PackageManagerService implements PackageSender, TestUtilityService // TODO(b/261957226): centralise this logic in DPM boolean isPackageDeviceAdmin(String packageName, int userId) { final IDevicePolicyManager dpm = getDevicePolicyManager(); + final DevicePolicyManagerInternal dpmi = + mInjector.getLocalService(DevicePolicyManagerInternal.class); try { - if (dpm != null) { + if (dpm != null && dpmi != null) { final ComponentName deviceOwnerComponentName = dpm.getDeviceOwnerComponent( /* callingUserOnly =*/ false); final String deviceOwnerPackageName = deviceOwnerComponentName == null ? null @@ -3396,7 +3399,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService if (dpm.packageHasActiveAdmins(packageName, users[i])) { return true; } - if (isDeviceManagementRoleHolder(packageName, users[i])) { + if (isDeviceManagementRoleHolder(packageName, users[i]) + && dpmi.isUserOrganizationManaged(users[i])) { return true; } } diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 4c9be21f1386..1f672a093b38 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -5575,18 +5575,18 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile } if (!paths.getOverlayPaths().isEmpty()) { pw.print(prefix); - pw.println(" "); + pw.print(" "); pw.print(libOverlayPaths.getKey()); pw.println(" overlay paths:"); for (String path : paths.getOverlayPaths()) { pw.print(prefix); - pw.print(" "); + pw.print(" "); pw.println(path); } } if (!paths.getResourceDirs().isEmpty()) { pw.print(prefix); - pw.println(" "); + pw.print(" "); pw.print(libOverlayPaths.getKey()); pw.println(" legacy overlay paths:"); for (String path : paths.getResourceDirs()) { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 25468faa084d..a3ff1952205f 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -588,7 +588,7 @@ public class ShortcutService extends IShortcutService.Stub { void handleOnDefaultLauncherChanged(int userId) { if (DEBUG) { - Slog.v(TAG, "Default launcher changed for user: " + userId); + Slog.v(TAG, "Default launcher changed for userId=" + userId); } // Default launcher is removed or changed, revoke all URI permissions. @@ -712,7 +712,7 @@ public class ShortcutService extends IShortcutService.Stub { /** lifecycle event */ void handleUnlockUser(int userId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "handleUnlockUser: user=" + userId); + Slog.d(TAG, "handleUnlockUser: userId=" + userId); } synchronized (mUnlockedUsers) { mUnlockedUsers.put(userId, true); @@ -739,7 +739,7 @@ public class ShortcutService extends IShortcutService.Stub { /** lifecycle event */ void handleStopUser(int userId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "handleStopUser: user=" + userId); + Slog.d(TAG, "handleStopUser: userId=" + userId); } Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "shortcutHandleStopUser"); synchronized (mServiceLock) { @@ -755,7 +755,7 @@ public class ShortcutService extends IShortcutService.Stub { @GuardedBy("mServiceLock") private void unloadUserLocked(int userId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "unloadUserLocked: user=" + userId); + Slog.d(TAG, "unloadUserLocked: userId=" + userId); } // Cancel any ongoing background tasks. getUserShortcutsLocked(userId).cancelAllInFlightTasks(); @@ -1221,7 +1221,7 @@ public class ShortcutService extends IShortcutService.Stub { private void scheduleSaveInner(@UserIdInt int userId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "Scheduling to save for " + userId); + Slog.d(TAG, "Scheduling to save for userId=" + userId); } synchronized (mServiceLock) { if (!mDirtyUserIds.contains(userId)) { @@ -1333,7 +1333,7 @@ public class ShortcutService extends IShortcutService.Stub { // Requires mServiceLock held, but "Locked" prefix would look weird so we just say "L". void throwIfUserLockedL(@UserIdInt int userId) { if (!isUserUnlockedL(userId)) { - throw new IllegalStateException("User " + userId + " is locked or not running"); + throw new IllegalStateException("User (with userId=" + userId + ") is locked or not running"); } } @@ -1720,7 +1720,7 @@ public class ShortcutService extends IShortcutService.Stub { // Otherwise, make sure the arguments are valid. if (UserHandle.getUserId(callingUid) != userId) { - throw new SecurityException("Invalid user-ID"); + throw new SecurityException("Invalid userId"); } } @@ -1735,7 +1735,7 @@ public class ShortcutService extends IShortcutService.Stub { // Otherwise, make sure the arguments are valid. if (UserHandle.getUserId(callingUid) != userId) { - throw new SecurityException("Invalid user-ID"); + throw new SecurityException("Invalid userId"); } if (injectGetPackageUid(packageName, userId) != callingUid) { throw new SecurityException("Calling package name mismatch"); @@ -1755,7 +1755,7 @@ public class ShortcutService extends IShortcutService.Stub { } final int callingUid = injectBinderCallingUid(); if (UserHandle.getUserId(callingUid) != si.getUserId()) { - throw new SecurityException("User-ID in shortcut doesn't match the caller"); + throw new SecurityException("UserId in shortcut doesn't match the caller"); } } @@ -1822,7 +1822,7 @@ public class ShortcutService extends IShortcutService.Stub { final int userId = sp.getPackageUserId(); if (DEBUG) { Slog.d(TAG, String.format( - "Shortcut changes: package=%s, user=%d", packageName, userId)); + "Shortcut changes: package=%s, userId=%d", packageName, userId)); } injectPostToHandlerDebounced(sp, notifyListenerRunnable(packageName, userId)); notifyShortcutChangeCallbacks(packageName, userId, changedShortcuts, removedShortcuts); @@ -1832,7 +1832,7 @@ public class ShortcutService extends IShortcutService.Stub { private void notifyListeners(@NonNull final String packageName, @UserIdInt final int userId) { if (DEBUG) { Slog.d(TAG, String.format( - "Shortcut changes: package=%s, user=%d", packageName, userId)); + "Shortcut changes: package=%s, userId=%d", packageName, userId)); } injectPostToHandler(notifyListenerRunnable(packageName, userId)); } @@ -2736,14 +2736,14 @@ public class ShortcutService extends IShortcutService.Stub { void resetThrottlingInner(@UserIdInt int userId) { synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { - Log.w(TAG, "User " + userId + " is locked or not running"); + Log.w(TAG, "User (with userId=" + userId + ") is locked or not running"); return; } getUserShortcutsLocked(userId).resetThrottling(); } scheduleSaveUser(userId); - Slog.i(TAG, "ShortcutManager: throttling counter reset for user " + userId); + Slog.i(TAG, "ShortcutManager: throttling counter reset for userId=" + userId); } void resetAllThrottlingInner() { @@ -2755,7 +2755,7 @@ public class ShortcutService extends IShortcutService.Stub { @Override public void onApplicationActive(String packageName, int userId) { if (DEBUG) { - Slog.d(TAG, "onApplicationActive: package=" + packageName + " userid=" + userId); + Slog.d(TAG, "onApplicationActive: package=" + packageName + " userId=" + userId); } enforceResetThrottlingPermission(); synchronized (mServiceLock) { @@ -2822,7 +2822,7 @@ public class ShortcutService extends IShortcutService.Stub { if (defaultLauncher != null) { if (DEBUG) { - Slog.v(TAG, "Detected launcher: " + defaultLauncher + " user: " + userId); + Slog.v(TAG, "Detected launcher: " + defaultLauncher + " userId=" + userId); } return defaultLauncher.equals(packageName); } else { @@ -2875,11 +2875,11 @@ public class ShortcutService extends IShortcutService.Stub { if (defaultLauncher != null) { if (DEBUG) { Slog.v(TAG, "Default launcher from RoleManager: " + defaultLauncher - + " user: " + userId); + + " userId=" + userId); } user.setCachedLauncher(defaultLauncher); } else { - Slog.e(TAG, "Default launcher not found." + " user: " + userId); + Slog.e(TAG, "Default launcher not found." + " userId=" + userId); } return defaultLauncher; @@ -2974,7 +2974,7 @@ public class ShortcutService extends IShortcutService.Stub { int queryFlags, int userId, int callingPid, int callingUid) { if (DEBUG_REBOOT) { Slog.d(TAG, "Getting shortcuts for launcher= " + callingPackage - + "user=" + userId + " pkg=" + packageName); + + "userId=" + userId + " pkg=" + packageName); } final ArrayList<ShortcutInfo> ret = new ArrayList<>(); @@ -3793,7 +3793,7 @@ public class ShortcutService extends IShortcutService.Stub { if (!isUserUnlockedL(userId)) { if (DEBUG) { Slog.d(TAG, "Ignoring package broadcast " + action - + " for locked/stopped user " + userId); + + " for locked/stopped userId=" + userId); } return; } @@ -3814,10 +3814,10 @@ public class ShortcutService extends IShortcutService.Stub { switch (action) { case Intent.ACTION_PACKAGE_ADDED: if (replacing) { - Slog.d(TAG, "replacing package: " + packageName + " userId" + userId); + Slog.d(TAG, "replacing package: " + packageName + " userId=" + userId); handlePackageUpdateFinished(packageName, userId); } else { - Slog.d(TAG, "adding package: " + packageName + " userId" + userId); + Slog.d(TAG, "adding package: " + packageName + " userId=" + userId); handlePackageAdded(packageName, userId); } break; @@ -3825,21 +3825,21 @@ public class ShortcutService extends IShortcutService.Stub { if (!replacing || (replacing && archival)) { if (!replacing) { Slog.d(TAG, "removing package: " - + packageName + " userId" + userId); + + packageName + " userId=" + userId); } else if (archival) { Slog.d(TAG, "archiving package: " - + packageName + " userId" + userId); + + packageName + " userId=" + userId); } handlePackageRemoved(packageName, userId); } break; case Intent.ACTION_PACKAGE_CHANGED: - Slog.d(TAG, "changing package: " + packageName + " userId" + userId); + Slog.d(TAG, "changing package: " + packageName + " userId=" + userId); handlePackageChanged(packageName, userId); break; case Intent.ACTION_PACKAGE_DATA_CLEARED: Slog.d(TAG, "clearing data for package: " - + packageName + " userId" + userId); + + packageName + " userId=" + userId); handlePackageDataCleared(packageName, userId); break; } @@ -3902,7 +3902,7 @@ public class ShortcutService extends IShortcutService.Stub { if (!isPackageInstalled(spi.getPackageName(), spi.getPackageUserId())) { if (DEBUG) { Slog.d(TAG, "Uninstalled: " + spi.getPackageName() - + " user " + spi.getPackageUserId()); + + " userId=" + spi.getPackageUserId()); } gonePackages.add( UserPackage.of(spi.getPackageUserId(), spi.getPackageName())); @@ -3927,7 +3927,7 @@ public class ShortcutService extends IShortcutService.Stub { @GuardedBy("mServiceLock") private void rescanUpdatedPackagesLocked(@UserIdInt int userId, long lastScanTime) { if (DEBUG_REBOOT) { - Slog.d(TAG, "rescan updated package user=" + userId + " last scanned=" + lastScanTime); + Slog.d(TAG, "rescan updated package userId=" + userId + " last scanned=" + lastScanTime); } final ShortcutUser user = getUserShortcutsLocked(userId); @@ -3953,7 +3953,7 @@ public class ShortcutService extends IShortcutService.Stub { private void handlePackageAdded(String packageName, @UserIdInt int userId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, String.format("handlePackageAdded: %s user=%d", packageName, userId)); + Slog.d(TAG, String.format("handlePackageAdded: %s userId=%d", packageName, userId)); } synchronized (mServiceLock) { final ShortcutUser user = getUserShortcutsLocked(userId); @@ -3965,7 +3965,7 @@ public class ShortcutService extends IShortcutService.Stub { private void handlePackageUpdateFinished(String packageName, @UserIdInt int userId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, String.format("handlePackageUpdateFinished: %s user=%d", + Slog.d(TAG, String.format("handlePackageUpdateFinished: %s userId=%d", packageName, userId)); } synchronized (mServiceLock) { @@ -3981,7 +3981,7 @@ public class ShortcutService extends IShortcutService.Stub { private void handlePackageRemoved(String packageName, @UserIdInt int packageUserId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, String.format("handlePackageRemoved: %s user=%d", packageName, + Slog.d(TAG, String.format("handlePackageRemoved: %s userId=%d", packageName, packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ false); @@ -3991,7 +3991,7 @@ public class ShortcutService extends IShortcutService.Stub { private void handlePackageDataCleared(String packageName, int packageUserId) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, String.format("handlePackageDataCleared: %s user=%d", packageName, + Slog.d(TAG, String.format("handlePackageDataCleared: %s userId=%d", packageName, packageUserId)); } cleanUpPackageForAllLoadedUsers(packageName, packageUserId, /* appStillExists = */ true); @@ -4006,7 +4006,7 @@ public class ShortcutService extends IShortcutService.Stub { return; } if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, String.format("handlePackageChanged: %s user=%d", packageName, + Slog.d(TAG, String.format("handlePackageChanged: %s userId=%d", packageName, packageUserId)); } @@ -4193,7 +4193,7 @@ public class ShortcutService extends IShortcutService.Stub { private void forUpdatedPackages(@UserIdInt int userId, long lastScanTime, boolean afterOta, Consumer<ApplicationInfo> callback) { if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "forUpdatedPackages for user " + userId + ", lastScanTime=" + lastScanTime + Slog.d(TAG, "forUpdatedPackages for userId=" + userId + ", lastScanTime=" + lastScanTime + " afterOta=" + afterOta); } final List<PackageInfo> list = getInstalledPackages(userId); @@ -4302,7 +4302,7 @@ public class ShortcutService extends IShortcutService.Stub { return mContext.createContextAsUser(UserHandle.of(userId), /* flags */ 0) .getPackageManager().getResourcesForApplication(packageName); } catch (NameNotFoundException e) { - Slog.e(TAG, "Resources of package " + packageName + " for user " + userId + Slog.e(TAG, "Resources of package " + packageName + " for userId=" + userId + " not found"); return null; } finally { @@ -4512,17 +4512,17 @@ public class ShortcutService extends IShortcutService.Stub { public byte[] getBackupPayload(@UserIdInt int userId) { enforceSystem(); if (DEBUG) { - Slog.d(TAG, "Backing up user " + userId); + Slog.d(TAG, "Backing up user with userId=" + userId); } synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { - wtf("Can't backup: user " + userId + " is locked or not running"); + wtf("Can't backup: userId=" + userId + " is locked or not running"); return null; } final ShortcutUser user = getUserShortcutsLocked(userId); if (user == null) { - wtf("Can't backup: user not found: id=" + userId); + wtf("Can't backup: user not found: userId=" + userId); return null; } @@ -4562,11 +4562,11 @@ public class ShortcutService extends IShortcutService.Stub { public void applyRestore(byte[] payload, @UserIdInt int userId) { enforceSystem(); if (DEBUG || DEBUG_REBOOT) { - Slog.d(TAG, "Restoring user " + userId); + Slog.d(TAG, "Restoring user with userId=" + userId); } synchronized (mServiceLock) { if (!isUserUnlockedL(userId)) { - wtf("Can't restore: user " + userId + " is locked or not running"); + wtf("Can't restore: user (with userId=" + userId + ") is locked or not running"); return; } // Note we print the file timestamps in dumpsys too, but also printing the timestamp @@ -4989,7 +4989,7 @@ public class ShortcutService extends IShortcutService.Stub { mUserId = UserHandle.parseUserArg(getNextArgRequired()); if (!isUserUnlockedL(mUserId)) { throw new CommandException( - "User " + mUserId + " is not running or locked"); + "User (with userId=" + mUserId + ") is not running or locked"); } break; } @@ -5094,7 +5094,7 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); - Slog.i(TAG, "cmd: handleResetThrottling: user=" + mUserId); + Slog.i(TAG, "cmd: handleResetThrottling: userId=" + mUserId); resetThrottlingInner(mUserId); } @@ -5136,7 +5136,7 @@ public class ShortcutService extends IShortcutService.Stub { final String defaultLauncher = getDefaultLauncher(mUserId); if (defaultLauncher == null) { throw new CommandException( - "Failed to get the default launcher for user " + mUserId); + "Failed to get the default launcher for userId=" + mUserId); } // Get the class name of the component from PM to keep the old behaviour. @@ -5157,7 +5157,7 @@ public class ShortcutService extends IShortcutService.Stub { synchronized (mServiceLock) { parseOptionsLocked(/* takeUser =*/ true); - Slog.i(TAG, "cmd: handleUnloadUser: user=" + mUserId); + Slog.i(TAG, "cmd: handleUnloadUser: userId=" + mUserId); ShortcutService.this.handleStopUser(mUserId); } @@ -5168,7 +5168,7 @@ public class ShortcutService extends IShortcutService.Stub { parseOptionsLocked(/* takeUser =*/ true); final String packageName = getNextArgRequired(); - Slog.i(TAG, "cmd: handleClearShortcuts: user" + mUserId + ", " + packageName); + Slog.i(TAG, "cmd: handleClearShortcuts: userId=" + mUserId + ", " + packageName); ShortcutService.this.cleanUpPackageForAllLoadedUsers(packageName, mUserId, /* appStillExists = */ true); @@ -5180,7 +5180,7 @@ public class ShortcutService extends IShortcutService.Stub { parseOptionsLocked(/* takeUser =*/ true); final String packageName = getNextArgRequired(); - Slog.i(TAG, "cmd: handleGetShortcuts: user=" + mUserId + ", flags=" + Slog.i(TAG, "cmd: handleGetShortcuts: userId=" + mUserId + ", flags=" + mShortcutMatchFlags + ", package=" + packageName); final ShortcutUser user = ShortcutService.this.getUserShortcutsLocked(mUserId); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a683a8c54849..13901c1f2c23 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1194,11 +1194,11 @@ public class UserManagerService extends IUserManager.Stub { // Avoid marking pre-created users for removal. return; } - if (ui.lastLoggedInTime == 0 && ui.isGuest() && Resources.getSystem().getBoolean( - com.android.internal.R.bool.config_guestUserAutoCreated)) { - // Avoid marking auto-created but not-yet-logged-in guest user for removal. Because a - // new one will be created anyway, and this one doesn't have any personal data in it yet - // due to not being logged in. + if (ui.lastLoggedInTime == 0) { + // Avoid marking a not-yet-logged-in ephemeral user for removal, since it doesn't have + // any personal data in it yet due to not being logged in. + // This will also avoid marking an auto-created not-yet-logged-in ephemeral guest user + // for removal, which would be recreated again later in the boot anyway. return; } // Mark the user for removal. diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java index 7ed89728a005..027e69cbc09b 100644 --- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java +++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java @@ -17,7 +17,9 @@ package com.android.server.policy; import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser; +import static com.android.hardware.input.Flags.modifierShortcutManagerRefactor; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.role.RoleManager; @@ -37,6 +39,7 @@ import android.text.TextUtils; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.LongSparseArray; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.view.KeyCharacterMap; @@ -81,8 +84,8 @@ public class ModifierShortcutManager { private static final String ATTRIBUTE_SHIFT = "shift"; private static final String ATTRIBUTE_ROLE = "role"; - private final SparseArray<Intent> mIntentShortcuts = new SparseArray<>(); - private final SparseArray<Intent> mShiftShortcuts = new SparseArray<>(); + private final SparseArray<Intent> mCategoryShortcuts = new SparseArray<>(); + private final SparseArray<Intent> mShiftCategoryShortcuts = new SparseArray<>(); private final SparseArray<String> mRoleShortcuts = new SparseArray<String>(); private final SparseArray<String> mShiftRoleShortcuts = new SparseArray<String>(); private final Map<String, Intent> mRoleIntents = new HashMap<String, Intent>(); @@ -127,6 +130,7 @@ public class ModifierShortcutManager { private boolean mSearchKeyShortcutPending = false; private boolean mConsumeSearchKeyUp = true; private UserHandle mCurrentUser; + private final Map<Pair<Character, Boolean>, Bookmark> mBookmarks = new HashMap<>(); ModifierShortcutManager(Context context, Handler handler, UserHandle currentUser) { mContext = context; @@ -134,7 +138,14 @@ public class ModifierShortcutManager { RoleManager rm = mContext.getSystemService(RoleManager.class); rm.addOnRoleHoldersChangedListenerAsUser(mContext.getMainExecutor(), (String roleName, UserHandle user) -> { - mRoleIntents.remove(roleName); + if (modifierShortcutManagerRefactor()) { + mBookmarks.values().stream().filter(b -> + b instanceof RoleBookmark + && ((RoleBookmark) b).getRole().equals(roleName)) + .forEach(Bookmark::clearIntent); + } else { + mRoleIntents.remove(roleName); + } }, UserHandle.ALL); mCurrentUser = currentUser; mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); @@ -146,8 +157,26 @@ public class ModifierShortcutManager { // Role based shortcuts may resolve to different apps for different users // so clear the cache. - mRoleIntents.clear(); - mComponentIntents.clear(); + clearRoleIntents(); + clearComponentIntents(); + } + + void clearRoleIntents() { + if (modifierShortcutManagerRefactor()) { + mBookmarks.values().stream().filter(b -> + b instanceof RoleBookmark).forEach(Bookmark::clearIntent); + } else { + mRoleIntents.clear(); + } + } + + void clearComponentIntents() { + if (modifierShortcutManagerRefactor()) { + mBookmarks.values().stream().filter(b -> + b instanceof ComponentBookmark).forEach(Bookmark::clearIntent); + } else { + mComponentIntents.clear(); + } } /** @@ -176,77 +205,111 @@ public class ModifierShortcutManager { Intent shortcutIntent = null; - // If the Shift key is pressed, then search for the shift shortcuts. - SparseArray<Intent> shortcutMap = isShiftOn ? mShiftShortcuts : mIntentShortcuts; - // First try the exact keycode (with modifiers). int shortcutChar = kcm.get(keyCode, metaState); if (shortcutChar == 0) { return null; } - shortcutIntent = shortcutMap.get(shortcutChar); - if (shortcutIntent == null) { - // Next try the primary character on that key. - shortcutChar = Character.toLowerCase(kcm.getDisplayLabel(keyCode)); - if (shortcutChar == 0) { - return null; + if (modifierShortcutManagerRefactor()) { + Bookmark bookmark = mBookmarks.get(new Pair<>((char) shortcutChar, isShiftOn)); + if (bookmark == null) { + // Next try the primary character on that key. + shortcutChar = Character.toLowerCase(kcm.getDisplayLabel(keyCode)); + if (shortcutChar == 0) { + return null; + } + bookmark = mBookmarks.get(new Pair<>((char) shortcutChar, isShiftOn)); + } + + if (bookmark != null) { + Context context = modifierShortcutManagerMultiuser() + ? mContext.createContextAsUser(mCurrentUser, 0) : mContext; + shortcutIntent = bookmark.getIntent(context); + } else { + Log.d(TAG, "No bookmark found for " + + (isShiftOn ? "SHIFT+" : "") + (char) shortcutChar); } + } else { + // If the Shift key is pressed, then search for the shift shortcuts. + SparseArray<Intent> shortcutMap = isShiftOn + ? mShiftCategoryShortcuts : mCategoryShortcuts; shortcutIntent = shortcutMap.get(shortcutChar); - } - if (shortcutIntent == null) { - // Next check for role based shortcut with primary character. - String role = isShiftOn ? mShiftRoleShortcuts.get(shortcutChar) - : mRoleShortcuts.get(shortcutChar); - if (role != null) { - shortcutIntent = getRoleLaunchIntent(role); + if (shortcutIntent == null) { + // Next try the primary character on that key. + shortcutChar = Character.toLowerCase(kcm.getDisplayLabel(keyCode)); + if (shortcutChar == 0) { + return null; + } + shortcutIntent = shortcutMap.get(shortcutChar); } - } - if (modifierShortcutManagerMultiuser()) { if (shortcutIntent == null) { - // Next check component based shortcuts with primary character. - ComponentName component = isShiftOn - ? mShiftComponentShortcuts.get(shortcutChar) - : mComponentShortcuts.get(shortcutChar); - if (component != null) { - shortcutIntent = resolveComponentNameIntent(component); + // Next check for role based shortcut with primary character. + String role = isShiftOn ? mShiftRoleShortcuts.get(shortcutChar) + : mRoleShortcuts.get(shortcutChar); + if (role != null) { + shortcutIntent = getRoleLaunchIntent(role); + } + } + + if (modifierShortcutManagerMultiuser()) { + if (shortcutIntent == null) { + // Next check component based shortcuts with primary character. + ComponentName component = isShiftOn + ? mShiftComponentShortcuts.get(shortcutChar) + : mComponentShortcuts.get(shortcutChar); + if (component != null) { + shortcutIntent = resolveComponentNameIntent(component); + } } } } return shortcutIntent; } + @Nullable + private static Intent getRoleLaunchIntent(Context context, String role) { + Intent intent = null; + RoleManager rm = context.getSystemService(RoleManager.class); + PackageManager pm = context.getPackageManager(); + if (rm.isRoleAvailable(role)) { + String rolePackage = rm.getDefaultApplication(role); + if (rolePackage != null) { + intent = pm.getLaunchIntentForPackage(rolePackage); + if (intent != null) { + intent.putExtra(EXTRA_ROLE, role); + + } else { + Log.w(TAG, "No launch intent for role " + role); + } + } else { + Log.w(TAG, "No default application for role " + + role + " user=" + context.getUser()); + } + } else { + Log.w(TAG, "Role " + role + " is not available."); + } + return intent; + } + + @Nullable private Intent getRoleLaunchIntent(String role) { Intent intent = mRoleIntents.get(role); if (intent == null) { Context context = modifierShortcutManagerMultiuser() ? mContext.createContextAsUser(mCurrentUser, 0) : mContext; - RoleManager rm = context.getSystemService(RoleManager.class); - PackageManager pm = context.getPackageManager(); - if (rm.isRoleAvailable(role)) { - String rolePackage = rm.getDefaultApplication(role); - if (rolePackage != null) { - intent = pm.getLaunchIntentForPackage(rolePackage); - if (intent != null) { - intent.putExtra(EXTRA_ROLE, role); - mRoleIntents.put(role, intent); - } else { - Log.w(TAG, "No launch intent for role " + role); - } - } else { - Log.w(TAG, "No default application for role " + role); - } - } else { - Log.w(TAG, "Role " + role + " is not available."); + intent = getRoleLaunchIntent(context, role); + if (intent != null) { + mRoleIntents.put(role, intent); } } + return intent; } private void loadShortcuts() { - try { XmlResourceParser parser = mContext.getResources().getXml(R.xml.bookmarks); XmlUtils.beginDocument(parser, TAG_BOOKMARKS); @@ -276,57 +339,84 @@ public class ModifierShortcutManager { continue; } - final int shortcutChar = shortcutName.charAt(0); final boolean isShiftShortcut = (shiftName != null && shiftName.equals("true")); - final Intent intent; - if (packageName != null && className != null) { - if (roleName != null || categoryName != null) { - Log.w(TAG, "Cannot specify role or category when package and class" - + " are present for bookmark packageName=" + packageName - + " className=" + className + " shortcutChar=" + shortcutChar); - continue; + + if (modifierShortcutManagerRefactor()) { + final char shortcutChar = shortcutName.charAt(0); + Bookmark bookmark = null; + if (packageName != null && className != null) { + bookmark = new ComponentBookmark( + shortcutChar, isShiftShortcut, packageName, className); + } else if (categoryName != null) { + bookmark = new CategoryBookmark( + shortcutChar, isShiftShortcut, categoryName); + } else if (roleName != null) { + bookmark = new RoleBookmark(shortcutChar, isShiftShortcut, roleName); } - if (modifierShortcutManagerMultiuser()) { - ComponentName componentName = new ComponentName(packageName, className); + if (bookmark != null) { + Log.d(TAG, "adding shortcut " + bookmark + "shift=" + + isShiftShortcut + " char=" + shortcutChar); + mBookmarks.put(new Pair<>(shortcutChar, isShiftShortcut), bookmark); + } + } else { + final int shortcutChar = shortcutName.charAt(0); + if (packageName != null && className != null) { + if (roleName != null || categoryName != null) { + Log.w(TAG, "Cannot specify role or category when package and class" + + " are present for bookmark packageName=" + packageName + + " className=" + className + " shortcutChar=" + shortcutChar); + continue; + } + if (modifierShortcutManagerMultiuser()) { + ComponentName componentName = + new ComponentName(packageName, className); + if (isShiftShortcut) { + mShiftComponentShortcuts.put(shortcutChar, componentName); + } else { + mComponentShortcuts.put(shortcutChar, componentName); + } + } else { + Intent intent = resolveComponentNameIntent(packageName, className); + if (isShiftShortcut) { + mShiftCategoryShortcuts.put(shortcutChar, intent); + } else { + mCategoryShortcuts.put(shortcutChar, intent); + } + } + continue; + } else if (categoryName != null) { + if (roleName != null) { + Log.w(TAG, "Cannot specify role bookmark when category is present for" + + " bookmark shortcutChar=" + shortcutChar + + " category= " + categoryName); + continue; + } + Intent intent = Intent.makeMainSelectorActivity( + Intent.ACTION_MAIN, categoryName); + if (intent == null) { + Log.w(TAG, "Null selector intent for " + categoryName); + } else { + if (isShiftShortcut) { + mShiftCategoryShortcuts.put(shortcutChar, intent); + } else { + mCategoryShortcuts.put(shortcutChar, intent); + } + } + continue; + } else if (roleName != null) { + // We can't resolve the role at the time of this file being parsed as the + // device hasn't finished booting, so we will look it up lazily. if (isShiftShortcut) { - mShiftComponentShortcuts.put(shortcutChar, componentName); + mShiftRoleShortcuts.put(shortcutChar, roleName); } else { - mComponentShortcuts.put(shortcutChar, componentName); + mRoleShortcuts.put(shortcutChar, roleName); } continue; } else { - intent = resolveComponentNameIntent(packageName, className); - } - } else if (categoryName != null) { - if (roleName != null) { - Log.w(TAG, "Cannot specify role bookmark when category is present for" - + " bookmark shortcutChar=" + shortcutChar - + " category= " + categoryName); + Log.w(TAG, "Unable to add bookmark for shortcut " + shortcutName + + ": missing package/class, category or role attributes"); continue; } - intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, categoryName); - if (intent == null) { - Log.w(TAG, "Null selector intent for " + categoryName); - } - } else if (roleName != null) { - // We can't resolve the role at the time of this file being parsed as the - // device hasn't finished booting, so we will look it up lazily. - if (isShiftShortcut) { - mShiftRoleShortcuts.put(shortcutChar, roleName); - } else { - mRoleShortcuts.put(shortcutChar, roleName); - } - continue; - } else { - Log.w(TAG, "Unable to add bookmark for shortcut " + shortcutName - + ": missing package/class, category or role attributes"); - continue; - } - - if (isShiftShortcut) { - mShiftShortcuts.put(shortcutChar, intent); - } else { - mIntentShortcuts.put(shortcutChar, intent); } } } catch (XmlPullParserException | IOException e) { @@ -336,21 +426,35 @@ public class ModifierShortcutManager { @Nullable private Intent resolveComponentNameIntent(ComponentName componentName) { - Intent intent = mComponentIntents.get(componentName); - if (intent == null) { - intent = resolveComponentNameIntent( - componentName.getPackageName(), componentName.getClassName()); - if (intent != null) { - mComponentIntents.put(componentName, intent); + if (modifierShortcutManagerRefactor()) { + return null; + } else { + Intent intent = mComponentIntents.get(componentName); + if (intent == null) { + intent = resolveComponentNameIntent( + componentName.getPackageName(), componentName.getClassName()); + if (intent != null) { + mComponentIntents.put(componentName, intent); + } } + return intent; } - return intent; } @Nullable private Intent resolveComponentNameIntent(String packageName, String className) { - Context context = modifierShortcutManagerMultiuser() - ? mContext.createContextAsUser(mCurrentUser, 0) : mContext; + if (modifierShortcutManagerRefactor()) { + return null; + } else { + Context context = modifierShortcutManagerMultiuser() + ? mContext.createContextAsUser(mCurrentUser, 0) : mContext; + return resolveComponentNameIntent(context, packageName, className); + } + } + + @Nullable + private static Intent resolveComponentNameIntent( + Context context, String packageName, String className) { PackageManager pm = context.getPackageManager(); int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE; if (!modifierShortcutManagerMultiuser()) { @@ -562,64 +666,81 @@ public class ModifierShortcutManager { */ public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) { List<KeyboardShortcutInfo> shortcuts = new ArrayList(); - for (int i = 0; i < mIntentShortcuts.size(); i++) { - KeyboardShortcutInfo info = shortcutInfoFromIntent( - (char) (mIntentShortcuts.keyAt(i)), mIntentShortcuts.valueAt(i), false); - if (info != null) { - shortcuts.add(info); - } - } - - for (int i = 0; i < mShiftShortcuts.size(); i++) { - KeyboardShortcutInfo info = shortcutInfoFromIntent( - (char) (mShiftShortcuts.keyAt(i)), mShiftShortcuts.valueAt(i), true); - if (info != null) { - shortcuts.add(info); + if (modifierShortcutManagerRefactor()) { + for (Bookmark b : mBookmarks.values()) { + KeyboardShortcutInfo info = shortcutInfoFromIntent( + b.getShortcutChar(), b.getIntent(mContext), b.isShift()); + if (info != null) { + shortcuts.add(info); + } } - } - - for (int i = 0; i < mRoleShortcuts.size(); i++) { - String role = mRoleShortcuts.valueAt(i); - KeyboardShortcutInfo info = shortcutInfoFromIntent( - (char) (mRoleShortcuts.keyAt(i)), getRoleLaunchIntent(role), false); - if (info != null) { - shortcuts.add(info); + } else { + for (int i = 0; i < mCategoryShortcuts.size(); i++) { + KeyboardShortcutInfo info = shortcutInfoFromIntent( + (char) (mCategoryShortcuts.keyAt(i)), + mCategoryShortcuts.valueAt(i), + false); + if (info != null) { + shortcuts.add(info); + } } - } - for (int i = 0; i < mShiftRoleShortcuts.size(); i++) { - String role = mShiftRoleShortcuts.valueAt(i); - KeyboardShortcutInfo info = shortcutInfoFromIntent( - (char) (mShiftRoleShortcuts.keyAt(i)), getRoleLaunchIntent(role), true); - if (info != null) { - shortcuts.add(info); + for (int i = 0; i < mShiftCategoryShortcuts.size(); i++) { + KeyboardShortcutInfo info = shortcutInfoFromIntent( + (char) (mShiftCategoryShortcuts.keyAt(i)), + mShiftCategoryShortcuts.valueAt(i), + true); + if (info != null) { + shortcuts.add(info); + } } - } - if (modifierShortcutManagerMultiuser()) { - for (int i = 0; i < mComponentShortcuts.size(); i++) { - ComponentName component = mComponentShortcuts.valueAt(i); + for (int i = 0; i < mRoleShortcuts.size(); i++) { + String role = mRoleShortcuts.valueAt(i); KeyboardShortcutInfo info = shortcutInfoFromIntent( - (char) (mComponentShortcuts.keyAt(i)), - resolveComponentNameIntent(component), + (char) (mRoleShortcuts.keyAt(i)), + getRoleLaunchIntent(role), false); if (info != null) { shortcuts.add(info); } } - for (int i = 0; i < mShiftComponentShortcuts.size(); i++) { - ComponentName component = mShiftComponentShortcuts.valueAt(i); + for (int i = 0; i < mShiftRoleShortcuts.size(); i++) { + String role = mShiftRoleShortcuts.valueAt(i); KeyboardShortcutInfo info = shortcutInfoFromIntent( - (char) (mShiftComponentShortcuts.keyAt(i)), - resolveComponentNameIntent(component), + (char) (mShiftRoleShortcuts.keyAt(i)), + getRoleLaunchIntent(role), true); if (info != null) { shortcuts.add(info); } } - } + if (modifierShortcutManagerMultiuser()) { + for (int i = 0; i < mComponentShortcuts.size(); i++) { + ComponentName component = mComponentShortcuts.valueAt(i); + KeyboardShortcutInfo info = shortcutInfoFromIntent( + (char) (mComponentShortcuts.keyAt(i)), + resolveComponentNameIntent(component), + false); + if (info != null) { + shortcuts.add(info); + } + } + + for (int i = 0; i < mShiftComponentShortcuts.size(); i++) { + ComponentName component = mShiftComponentShortcuts.valueAt(i); + KeyboardShortcutInfo info = shortcutInfoFromIntent( + (char) (mShiftComponentShortcuts.keyAt(i)), + resolveComponentNameIntent(component), + true); + if (info != null) { + shortcuts.add(info); + } + } + } + } return new KeyboardShortcutGroup( mContext.getString(R.string.keyboard_shortcut_group_applications), shortcuts); @@ -800,57 +921,171 @@ public class ModifierShortcutManager { void dump(String prefix, PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ", prefix); ipw.println("ModifierShortcutManager shortcuts:"); + if (modifierShortcutManagerRefactor()) { + ipw.increaseIndent(); + for (Bookmark b : mBookmarks.values()) { + boolean isShift = b.isShift(); + char shortcutChar = b.getShortcutChar(); + Context context = modifierShortcutManagerMultiuser() + ? mContext.createContextAsUser(mCurrentUser, 0) : mContext; + + Intent intent = b.getIntent(context); + ipw.print(isShift ? "SHIFT+" : ""); + ipw.println(shortcutChar + " " + intent); + ipw.increaseIndent(); + ipw.increaseIndent(); + KeyboardShortcutInfo info = shortcutInfoFromIntent(shortcutChar, intent, isShift); + if (info != null) { + ipw.println("Resolves to: " + info.getLabel()); + } else { + ipw.println("<No KeyboardShortcutInfo available for this shortcut>"); + } + ipw.decreaseIndent(); + ipw.decreaseIndent(); + } + } else { + ipw.increaseIndent(); + ipw.println("Roles"); + ipw.increaseIndent(); + for (int i = 0; i < mRoleShortcuts.size(); i++) { + String role = mRoleShortcuts.valueAt(i); + char shortcutChar = (char) mRoleShortcuts.keyAt(i); + Intent intent = getRoleLaunchIntent(role); + ipw.println(shortcutChar + " " + role + " " + intent); + } + + for (int i = 0; i < mShiftRoleShortcuts.size(); i++) { + String role = mShiftRoleShortcuts.valueAt(i); + char shortcutChar = (char) mShiftRoleShortcuts.keyAt(i); + Intent intent = getRoleLaunchIntent(role); + ipw.println("SHIFT+" + shortcutChar + " " + role + " " + intent); + } + + ipw.decreaseIndent(); + ipw.println("Selectors"); + ipw.increaseIndent(); + for (int i = 0; i < mCategoryShortcuts.size(); i++) { + char shortcutChar = (char) mCategoryShortcuts.keyAt(i); + Intent intent = mCategoryShortcuts.valueAt(i); + ipw.println(shortcutChar + " " + intent); + } + + for (int i = 0; i < mShiftCategoryShortcuts.size(); i++) { + char shortcutChar = (char) mShiftCategoryShortcuts.keyAt(i); + Intent intent = mShiftCategoryShortcuts.valueAt(i); + ipw.println("SHIFT+" + shortcutChar + " " + intent); + + } - ipw.increaseIndent(); - ipw.println("Roles"); - ipw.increaseIndent(); - for (int i = 0; i < mRoleShortcuts.size(); i++) { - String role = mRoleShortcuts.valueAt(i); - char shortcutChar = (char) mRoleShortcuts.keyAt(i); - Intent intent = getRoleLaunchIntent(role); - ipw.println(shortcutChar + " " + role + " " + intent); + if (modifierShortcutManagerMultiuser()) { + ipw.decreaseIndent(); + ipw.println("ComponentNames"); + ipw.increaseIndent(); + for (int i = 0; i < mComponentShortcuts.size(); i++) { + char shortcutChar = (char) mComponentShortcuts.keyAt(i); + ComponentName component = mComponentShortcuts.valueAt(i); + Intent intent = resolveComponentNameIntent(component); + ipw.println(shortcutChar + " " + component + " " + intent); + } + + for (int i = 0; i < mShiftComponentShortcuts.size(); i++) { + char shortcutChar = (char) mShiftComponentShortcuts.keyAt(i); + ComponentName component = mShiftComponentShortcuts.valueAt(i); + Intent intent = resolveComponentNameIntent(component); + ipw.println("SHIFT+" + shortcutChar + " " + component + " " + intent); + } + } + } + } + + private abstract static class Bookmark { + private final char mShortcutChar; + private final boolean mShift; + protected Intent mIntent; + + Bookmark(char shortcutChar, boolean shift) { + mShortcutChar = shortcutChar; + mShift = shift; } - for (int i = 0; i < mShiftRoleShortcuts.size(); i++) { - String role = mShiftRoleShortcuts.valueAt(i); - char shortcutChar = (char) mShiftRoleShortcuts.keyAt(i); - Intent intent = getRoleLaunchIntent(role); - ipw.println("SHIFT+" + shortcutChar + " " + role + " " + intent); + public char getShortcutChar() { + return mShortcutChar; } - ipw.decreaseIndent(); - ipw.println("Selectors"); - ipw.increaseIndent(); - for (int i = 0; i < mIntentShortcuts.size(); i++) { - char shortcutChar = (char) mIntentShortcuts.keyAt(i); - Intent intent = mIntentShortcuts.valueAt(i); - ipw.println(shortcutChar + " " + intent); + public boolean isShift() { + return mShift; } - for (int i = 0; i < mShiftShortcuts.size(); i++) { - char shortcutChar = (char) mShiftShortcuts.keyAt(i); - Intent intent = mShiftShortcuts.valueAt(i); - ipw.println("SHIFT+" + shortcutChar + " " + intent); + public abstract Intent getIntent(Context context); + public void clearIntent() { + mIntent = null; } - if (modifierShortcutManagerMultiuser()) { - ipw.decreaseIndent(); - ipw.println("ComponentNames"); - ipw.increaseIndent(); - for (int i = 0; i < mComponentShortcuts.size(); i++) { - char shortcutChar = (char) mComponentShortcuts.keyAt(i); - ComponentName component = mComponentShortcuts.valueAt(i); - Intent intent = resolveComponentNameIntent(component); - ipw.println(shortcutChar + " " + component + " " + intent); + } + + private static final class RoleBookmark extends Bookmark { + private final String mRole; + + RoleBookmark(char shortcutChar, boolean shift, String role) { + super(shortcutChar, shift); + mRole = role; + } + + public String getRole() { + return mRole; + } + + @Nullable + @Override + public Intent getIntent(Context context) { + if (mIntent != null) { + return mIntent; + } + mIntent = getRoleLaunchIntent(context, mRole); + return mIntent; + } + } + + private static final class CategoryBookmark extends Bookmark { + private final String mCategory; + + CategoryBookmark(char shortcutChar, boolean shift, String category) { + super(shortcutChar, shift); + mCategory = category; + } + + @NonNull + @Override + public Intent getIntent(Context context) { + if (mIntent != null) { + return mIntent; } - for (int i = 0; i < mShiftComponentShortcuts.size(); i++) { - char shortcutChar = (char) mShiftComponentShortcuts.keyAt(i); - ComponentName component = mShiftComponentShortcuts.valueAt(i); - Intent intent = resolveComponentNameIntent(component); - ipw.println("SHIFT+" + shortcutChar + " " + component + " " + intent); + mIntent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, mCategory); + return mIntent; + } + } + + private static final class ComponentBookmark extends Bookmark { + private final String mPackageName; + private final String mClassName; + + ComponentBookmark( + char shortcutChar, boolean shift, String packageName, String className) { + super(shortcutChar, shift); + mPackageName = packageName; + mClassName = className; + } + + @Nullable + @Override + public Intent getIntent(Context context) { + if (mIntent != null) { + return mIntent; } + mIntent = resolveComponentNameIntent(context, mPackageName, mClassName); + return mIntent; } } } diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 1a2a196fe4e8..303828f94e8a 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -1064,9 +1064,9 @@ public class Notifier { private void notifyWakeLockListener(IWakeLockCallback callback, String tag, boolean isEnabled, int ownerUid, int ownerPid, int flags, WorkSource workSource, String packageName, String historyTag) { + long currentTime = mInjector.currentTimeMillis(); mHandler.post(() -> { if (mFlags.improveWakelockLatency()) { - long currentTime = mInjector.currentTimeMillis(); if (isEnabled) { notifyWakelockAcquisition(tag, ownerUid, ownerPid, flags, workSource, packageName, historyTag, currentTime); diff --git a/services/core/java/com/android/server/power/WakeLockLog.java b/services/core/java/com/android/server/power/WakeLockLog.java index 968ff59ad8fc..eda222e71c9e 100644 --- a/services/core/java/com/android/server/power/WakeLockLog.java +++ b/services/core/java/com/android/server/power/WakeLockLog.java @@ -19,6 +19,7 @@ package com.android.server.power; import android.content.Context; import android.content.pm.PackageManager; import android.os.PowerManager; +import android.os.Process; import android.text.TextUtils; import android.util.Slog; import android.util.SparseArray; @@ -122,6 +123,9 @@ final class WakeLockLog { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); + @VisibleForTesting + static final String SYSTEM_PACKAGE_NAME = "System"; + /** * Lock protects WakeLockLog.dump (binder thread) from conflicting with changes to the log * happening on the background thread. @@ -516,21 +520,26 @@ final class WakeLockLog { return; } - String[] packages; - if (uidToPackagesCache.contains(tag.ownerUid)) { - packages = uidToPackagesCache.get(tag.ownerUid); - } else { - packages = packageManager.getPackagesForUid(tag.ownerUid); - uidToPackagesCache.put(tag.ownerUid, packages); + if (tag.ownerUid == Process.SYSTEM_UID) { + packageName = SYSTEM_PACKAGE_NAME; } + else { + String[] packages; + if (uidToPackagesCache.contains(tag.ownerUid)) { + packages = uidToPackagesCache.get(tag.ownerUid); + } else { + packages = packageManager.getPackagesForUid(tag.ownerUid); + uidToPackagesCache.put(tag.ownerUid, packages); + } - if (packages != null && packages.length > 0) { - packageName = packages[0]; - if (packages.length > 1) { - StringBuilder sb = new StringBuilder(); - sb.append(packageName) - .append(",..."); - packageName = sb.toString(); + if (packages != null && packages.length > 0) { + packageName = packages[0]; + if (packages.length > 1) { + StringBuilder sb = new StringBuilder(); + sb.append(packageName) + .append(",..."); + packageName = sb.toString(); + } } } } diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 6b3b5bde851b..67900f843063 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -251,7 +251,7 @@ public final class TvInputManagerService extends SystemService { } private void registerBroadcastReceivers() { - PackageMonitor monitor = new PackageMonitor() { + PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) { private void buildTvInputList(String[] packages) { int userId = getChangingUserId(); synchronized (mLock) { diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index edd2fa9a4e44..6a7fc6dcf7cd 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -519,7 +519,7 @@ public class TvInteractiveAppManagerService extends SystemService { } private void registerBroadcastReceivers() { - PackageMonitor monitor = new PackageMonitor() { + PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) { private void buildTvInteractiveAppServiceList(String[] packages) { int userId = getChangingUserId(); synchronized (mLock) { diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index c4d601d03652..67401530763b 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -19,14 +19,12 @@ package com.android.server.webkit; import static android.webkit.Flags.updateServiceV2; import android.app.ActivityManager; -import android.app.AppGlobals; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.UserInfo; import android.content.res.XmlResourceParser; import android.os.Build; import android.os.RemoteException; @@ -79,7 +77,7 @@ public class SystemImpl implements SystemInterface { XmlResourceParser parser = null; List<WebViewProviderInfo> webViewProviders = new ArrayList<WebViewProviderInfo>(); try { - parser = AppGlobals.getInitialApplication().getResources().getXml( + parser = mContext.getResources().getXml( com.android.internal.R.xml.config_webview_packages); XmlUtils.beginDocument(parser, TAG_START); while(true) { @@ -148,7 +146,7 @@ public class SystemImpl implements SystemInterface { } public long getFactoryPackageVersion(String packageName) throws NameNotFoundException { - PackageManager pm = AppGlobals.getInitialApplication().getPackageManager(); + PackageManager pm = mContext.getPackageManager(); return pm.getPackageInfo(packageName, PackageManager.MATCH_FACTORY_ONLY) .getLongVersionCode(); } @@ -203,47 +201,48 @@ public class SystemImpl implements SystemInterface { @Override public void enablePackageForAllUsers(String packageName, boolean enable) { UserManager userManager = mContext.getSystemService(UserManager.class); - for(UserInfo userInfo : userManager.getUsers()) { - enablePackageForUser(packageName, enable, userInfo.id); + for (UserHandle user : userManager.getUserHandles(false)) { + enablePackageForUser(packageName, enable, user); } } - private void enablePackageForUser(String packageName, boolean enable, int userId) { + private void enablePackageForUser(String packageName, boolean enable, UserHandle user) { + Context contextAsUser = mContext.createContextAsUser(user, 0); + PackageManager pm = contextAsUser.getPackageManager(); try { - AppGlobals.getPackageManager().setApplicationEnabledSetting( + pm.setApplicationEnabledSetting( packageName, enable ? PackageManager.COMPONENT_ENABLED_STATE_DEFAULT : - PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0, - userId, null); - } catch (RemoteException | IllegalArgumentException e) { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0); + } catch (IllegalArgumentException e) { Log.w(TAG, "Tried to " + (enable ? "enable " : "disable ") + packageName - + " for user " + userId + ": " + e); + + " for user " + user + ": " + e); } } @Override public void installExistingPackageForAllUsers(String packageName) { UserManager userManager = mContext.getSystemService(UserManager.class); - for (UserInfo userInfo : userManager.getUsers()) { - installPackageForUser(packageName, userInfo.id); + for (UserHandle user : userManager.getUserHandles(false)) { + installPackageForUser(packageName, user); } } - private void installPackageForUser(String packageName, int userId) { - final Context contextAsUser = mContext.createContextAsUser(UserHandle.of(userId), 0); - final PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller(); + private void installPackageForUser(String packageName, UserHandle user) { + Context contextAsUser = mContext.createContextAsUser(user, 0); + PackageInstaller installer = contextAsUser.getPackageManager().getPackageInstaller(); installer.installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN, null); } @Override public boolean systemIsDebuggable() { - return Build.IS_DEBUGGABLE; + return Build.isDebuggable(); } @Override public PackageInfo getPackageInfoForProvider(WebViewProviderInfo configInfo) throws NameNotFoundException { - PackageManager pm = AppGlobals.getInitialApplication().getPackageManager(); + PackageManager pm = mContext.getPackageManager(); return pm.getPackageInfo(configInfo.packageName, PACKAGE_FLAGS); } @@ -327,5 +326,5 @@ public class SystemImpl implements SystemInterface { // flags declaring we want extra info from the package manager for webview providers private final static int PACKAGE_FLAGS = PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES | PackageManager.GET_SHARED_LIBRARY_FILES - | PackageManager.MATCH_DEBUG_TRIAGED_MISSING | PackageManager.MATCH_ANY_USER; + | PackageManager.MATCH_ANY_USER; } diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 2ce1aa422601..fb5c1154c7f0 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -838,12 +838,13 @@ class ActivityMetricsLogger { } if (android.app.Flags.appStartInfoTimestamps()) { + final int pid = r.getPid(); // Log here to match StatsD for time to first frame. mLoggerHandler.post( () -> mSupervisor.mService.mWindowManager.mAmInternal.addStartInfoTimestamp( ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME, - timestampNs, r.getUid(), r.getPid(), - info.mLastLaunchedActivity.mUserId)); + timestampNs, infoSnapshot.applicationInfo.uid, pid, + infoSnapshot.userId)); } return infoSnapshot; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index ebdf52cc9037..e562ea84d001 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8152,6 +8152,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ @Override protected int getOverrideOrientation() { + if (mWmService.mConstants.mIgnoreActivityOrientationRequest) { + return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + } return mAppCompatController.getOrientationPolicy() .overrideOrientationIfNeeded(super.getOverrideOrientation()); } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 3710f7fcb7cd..28dbc3a664a5 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -882,6 +882,7 @@ class BackNavigationController { } else { if (mAnimationHandler.mPrepareCloseTransition != null) { Slog.e(TAG, "Gesture animation is applied on another transition?"); + return; } mAnimationHandler.mPrepareCloseTransition = transition; if (!migratePredictToTransition) { diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 20c5f02aaee2..2259b5a5b08c 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -21,10 +21,10 @@ import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT; import static android.app.ActivityOptions.BackgroundActivityStartMode; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; @@ -39,13 +39,15 @@ import static com.android.server.wm.ActivityStarter.ASM_RESTRICTIONS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ACTIVITY_STARTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; +import static com.android.window.flags.Flags.balAdditionalStartModes; import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; -import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck; +import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balRequireOptInSameUid; import static com.android.window.flags.Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid; @@ -84,6 +86,7 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.Preconditions; import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; +import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -107,6 +110,17 @@ public class BackgroundActivityStartController { private static final int ASM_GRACEPERIOD_MAX_REPEATS = 5; private static final int NO_PROCESS_UID = -1; + private static final BalCheckConfiguration BAL_CHECK_FOREGROUND = new BalCheckConfiguration( + /* isCheckingForFgsStarts */ false, + /* checkVisibility */ true, + /* checkOtherExemptions */ false, + ACTIVITY_BG_START_GRACE_PERIOD_MS); + private static final BalCheckConfiguration BAL_CHECK_BACKGROUND = new BalCheckConfiguration( + /* isCheckingForFgsStarts */ false, + /* checkVisibility */ false, + /* checkOtherExemptions */ true, + ACTIVITY_BG_START_GRACE_PERIOD_MS); + static final String AUTO_OPT_IN_NOT_PENDING_INTENT = "notPendingIntent"; static final String AUTO_OPT_IN_CALL_FOR_RESULT = "callForResult"; static final String AUTO_OPT_IN_SAME_UID = "sameUid"; @@ -412,6 +426,8 @@ public class BackgroundActivityStartController { int callingUid, String callingPackage, ActivityOptions checkedOptions) { switch (checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()) { case MODE_BACKGROUND_ACTIVITY_START_ALLOWED: + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE: + case MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS: return BackgroundStartPrivileges.ALLOW_BAL; case MODE_BACKGROUND_ACTIVITY_START_DENIED: return BackgroundStartPrivileges.NONE; @@ -752,7 +768,7 @@ public class BackgroundActivityStartController { // PendingIntents is null). BalVerdict resultForRealCaller = state.callerIsRealCaller() && resultForCaller.allows() ? resultForCaller - : checkBackgroundActivityStartAllowedBySender(state) + : checkBackgroundActivityStartAllowedByRealCaller(state) .setBasedOnRealCaller(); state.setResultForRealCaller(resultForRealCaller); @@ -827,6 +843,37 @@ public class BackgroundActivityStartController { * or {@link #BAL_BLOCK} if the launch should be blocked */ BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) { + if (state.isPendingIntent()) { + // PendingIntents should mostly be allowed by the sender (real caller) or a permission + // the creator of the PendingIntent has. Visibility should be the exceptional case, so + // test it last (this does not change the result, just the bal code). + BalVerdict result = BalVerdict.BLOCK; + if (!(balAdditionalStartModes() + && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) { + result = checkBackgroundActivityStartAllowedByCallerInBackground(state); + } + if (result == BalVerdict.BLOCK) { + result = checkBackgroundActivityStartAllowedByCallerInForeground(state); + + } + return result; + } else { + BalVerdict result = checkBackgroundActivityStartAllowedByCallerInForeground(state); + if (result == BalVerdict.BLOCK && !(balAdditionalStartModes() + && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) { + result = checkBackgroundActivityStartAllowedByCallerInBackground(state); + } + return result; + } + } + + /** + * @return A code denoting which BAL rule allows an activity to be started, + * or {@link #BAL_BLOCK} if the launch should be blocked + */ + BalVerdict checkBackgroundActivityStartAllowedByCallerInForeground(BalState state) { // This is used to block background activity launch even if the app is still // visible to user after user clicking home button. @@ -842,7 +889,16 @@ public class BackgroundActivityStartController { return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW, /*background*/ false, "callingUid has non-app visible window"); } + // Don't abort if the callerApp or other processes of that uid are considered to be in the + // foreground. + return checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_FOREGROUND); + } + /** + * @return A code denoting which BAL rule allows an activity to be started, + * or {@link #BAL_BLOCK} if the launch should be blocked + */ + BalVerdict checkBackgroundActivityStartAllowedByCallerInBackground(BalState state) { // don't abort for the most important UIDs final int callingAppId = UserHandle.getAppId(state.mCallingUid); if (state.mCallingUid == Process.ROOT_UID @@ -922,25 +978,29 @@ public class BackgroundActivityStartController { "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted"); } - // If we don't have callerApp at this point, no caller was provided to startActivity(). - // That's the case for PendingIntent-based starts, since the creator's process might not be - // up and alive. // Don't abort if the callerApp or other processes of that uid are allowed in any way. - BalVerdict callerAppAllowsBal = checkProcessAllowsBal(state.mCallerApp, state); - if (callerAppAllowsBal.allows()) { - return callerAppAllowsBal; - } - - // If we are here, it means all exemptions based on the creator failed - return BalVerdict.BLOCK; + return checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_BACKGROUND); } /** * @return A code denoting which BAL rule allows an activity to be started, * or {@link #BAL_BLOCK} if the launch should be blocked */ - BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) { + BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) { + BalVerdict result = checkBackgroundActivityStartAllowedByRealCallerInForeground(state); + if (result == BalVerdict.BLOCK && !(balAdditionalStartModes() + && state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) { + result = checkBackgroundActivityStartAllowedByRealCallerInBackground(state); + } + return result; + } + /** + * @return A code denoting which BAL rule allows an activity to be started, + * or {@link #BAL_BLOCK} if the launch should be blocked + */ + BalVerdict checkBackgroundActivityStartAllowedByRealCallerInForeground(BalState state) { // Normal apps with visible app window will be allowed to start activity if app switching // is allowed, or apps like live wallpaper with non app visible window will be allowed. // The home app can start apps even if app switches are usually disallowed. @@ -966,6 +1026,16 @@ public class BackgroundActivityStartController { } } + // Don't abort if the realCallerApp or other processes of that uid are considered to be in + // the foreground. + return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND); + } + + /** + * @return A code denoting which BAL rule allows an activity to be started, + * or {@link #BAL_BLOCK} if the launch should be blocked + */ + BalVerdict checkBackgroundActivityStartAllowedByRealCallerInBackground(BalState state) { if (state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) { @@ -992,14 +1062,7 @@ public class BackgroundActivityStartController { } // don't abort if the callerApp or other processes of that uid are allowed in any way - BalVerdict realCallerAppAllowsBal = - checkProcessAllowsBal(state.mRealCallerApp, state); - if (realCallerAppAllowsBal.allows()) { - return realCallerAppAllowsBal; - } - - // If we are here, it means all exemptions based on PI sender failed - return BalVerdict.BLOCK; + return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND); } @VisibleForTesting boolean hasBalPermission(int uid, int pid) { @@ -1015,13 +1078,13 @@ public class BackgroundActivityStartController { * exceptions. */ @VisibleForTesting BalVerdict checkProcessAllowsBal(WindowProcessController app, - BalState state) { + BalState state, BalCheckConfiguration balCheckConfiguration) { if (app == null) { return BalVerdict.BLOCK; } // first check the original calling process final BalVerdict balAllowedForCaller = app - .areBackgroundActivityStartsAllowed(state.mAppSwitchState); + .areBackgroundActivityStartsAllowed(state.mAppSwitchState, balCheckConfiguration); if (balAllowedForCaller.allows()) { return balAllowedForCaller.withProcessInfo("callerApp process", app); } else { @@ -1033,7 +1096,7 @@ public class BackgroundActivityStartController { final WindowProcessController proc = uidProcesses.valueAt(i); if (proc != app) { BalVerdict balAllowedForUid = proc.areBackgroundActivityStartsAllowed( - state.mAppSwitchState); + state.mAppSwitchState, balCheckConfiguration); if (balAllowedForUid.allows()) { return balAllowedForUid.withProcessInfo("process", proc); } @@ -1685,6 +1748,21 @@ public class BackgroundActivityStartController { (state.mOriginatingPendingIntent != null)); } + if (finalVerdict.getRawCode() == BAL_ALLOW_GRACE_PERIOD) { + if (state.realCallerExplicitOptInOrAutoOptIn() + && state.mResultForRealCaller.allows() + && state.mResultForRealCaller.getRawCode() != BAL_ALLOW_GRACE_PERIOD) { + // real caller could allow with a different exemption + } else if (state.callerExplicitOptInOrAutoOptIn() && state.mResultForCaller.allows() + && state.mResultForCaller.getRawCode() != BAL_ALLOW_GRACE_PERIOD) { + // caller could allow with a different exemption + } else { + // log to determine grace period length distribution + Slog.wtf(TAG, "Activity start ONLY allowed by BAL_ALLOW_GRACE_PERIOD " + + finalVerdict.mMessage + ": " + state); + } + } + if (balImprovedMetrics()) { if (shouldLogStats(finalVerdict, state)) { String activityName; diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java index 4a870a3a5b6e..1073713cca52 100644 --- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java +++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java @@ -17,7 +17,6 @@ package com.android.server.wm; import static com.android.internal.util.Preconditions.checkArgument; -import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ACTIVITY_STARTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS; @@ -48,7 +47,6 @@ import android.os.SystemClock; import android.os.UserHandle; import android.util.ArrayMap; import android.util.IntArray; -import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.wm.BackgroundActivityStartController.BalVerdict; @@ -100,60 +98,75 @@ class BackgroundLaunchProcessController { mBackgroundActivityStartCallback = callback; } + record BalCheckConfiguration( + boolean isCheckingForFgsStart, + boolean checkVisibility, + boolean checkOtherExemptions, + long gracePeriod + ) { + } + + /** + * Check configuration for foreground service starts. + * + * The check executes all parts of the BAL checks and uses the same grace period, + * so FGS is allowed whenever BAL is allowed. + */ + static final BalCheckConfiguration CHECK_FOR_FGS_START = new BalCheckConfiguration( + /* isCheckingForFgsStarts */ true, + /* checkVisibility */ true, + /* checkOtherExemptions */ true, + ACTIVITY_BG_START_GRACE_PERIOD_MS); + BalVerdict areBackgroundActivityStartsAllowed( int pid, int uid, String packageName, - int appSwitchState, boolean isCheckingForFgsStart, + int appSwitchState, BalCheckConfiguration checkConfiguration, boolean hasActivityInVisibleTask, boolean hasBackgroundActivityStartPrivileges, long lastStopAppSwitchesTime, long lastActivityLaunchTime, long lastActivityFinishTime) { // Allow if the proc is instrumenting with background activity starts privs. - if (hasBackgroundActivityStartPrivileges) { + if (checkConfiguration.checkOtherExemptions && hasBackgroundActivityStartPrivileges) { return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true, "process instrumenting with background activity starts privileges"); } // Allow if the flag was explicitly set. - if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) { + if (checkConfiguration.checkOtherExemptions && isBackgroundStartAllowedByToken(uid, + packageName, checkConfiguration.isCheckingForFgsStart)) { return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_TOKEN : BAL_ALLOW_PERMISSION, /*background*/ true, "process allowed by token"); } // Allow if the caller is bound by a UID that's currently foreground. // But still respect the appSwitchState. - boolean allowBoundByForegroundUid = + if (checkConfiguration.checkVisibility && ( Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid() - ? appSwitchState != APP_SWITCH_DISALLOW && isBoundByForegroundUid() - : isBoundByForegroundUid(); - if (allowBoundByForegroundUid) { + ? appSwitchState != APP_SWITCH_DISALLOW && isBoundByForegroundUid() + : isBoundByForegroundUid())) { return new BalVerdict(balImprovedMetrics() ? BAL_ALLOW_BOUND_BY_FOREGROUND : BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false, "process bound by foreground uid"); } // Allow if the caller has an activity in any foreground task. - if (hasActivityInVisibleTask && appSwitchState != APP_SWITCH_DISALLOW) { + if (checkConfiguration.checkVisibility && hasActivityInVisibleTask + && appSwitchState != APP_SWITCH_DISALLOW) { return new BalVerdict(BAL_ALLOW_FOREGROUND, /*background*/ false, "process has activity in foreground task"); } // If app switching is not allowed, we ignore all the start activity grace period // exception so apps cannot start itself in onPause() after pressing home button. - if (appSwitchState == APP_SWITCH_ALLOW) { + if (checkConfiguration.checkOtherExemptions && appSwitchState == APP_SWITCH_ALLOW) { // Allow if any activity in the caller has either started or finished very recently, and // it must be started or finished after last stop app switches time. - final long now = SystemClock.uptimeMillis(); - if (now - lastActivityLaunchTime < ACTIVITY_BG_START_GRACE_PERIOD_MS - || now - lastActivityFinishTime < ACTIVITY_BG_START_GRACE_PERIOD_MS) { - // If activity is started and finished before stop app switch time, we should not - // let app to be able to start background activity even it's in grace period. - if (lastActivityLaunchTime > lastStopAppSwitchesTime - || lastActivityFinishTime > lastStopAppSwitchesTime) { + if (lastActivityLaunchTime > lastStopAppSwitchesTime + || lastActivityFinishTime > lastStopAppSwitchesTime) { + final long now = SystemClock.uptimeMillis(); + long timeSinceLastStartOrFinish = now - Math.max(lastActivityLaunchTime, + lastActivityFinishTime); + if (timeSinceLastStartOrFinish < checkConfiguration.gracePeriod) { return new BalVerdict(BAL_ALLOW_GRACE_PERIOD, /*background*/ true, - "within " + ACTIVITY_BG_START_GRACE_PERIOD_MS + "ms grace period"); + "within " + checkConfiguration.gracePeriod + "ms grace period (" + + timeSinceLastStartOrFinish + "ms)"); } - if (DEBUG_ACTIVITY_STARTS) { - Slog.d(TAG, "[Process(" + pid + ")] Activity start within " - + ACTIVITY_BG_START_GRACE_PERIOD_MS - + "ms grace period but also within stop app switch window"); - } - } } return BalVerdict.BLOCK; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 0597ed7a1c41..34bbe6ad8e21 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1835,7 +1835,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (mTransitionController.useShellTransitionsRotation()) { return ROTATION_UNDEFINED; } - final int activityOrientation = r.getOverrideOrientation(); + int activityOrientation = r.getOverrideOrientation(); if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM || shouldIgnoreOrientationRequest(activityOrientation)) { return ROTATION_UNDEFINED; @@ -1846,14 +1846,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */); if (nextCandidate != null) { r = nextCandidate; + activityOrientation = r.getOverrideOrientation(); } } - if (r.inMultiWindowMode() || r.getRequestedConfigurationOrientation(true /* forDisplay */) - == getConfiguration().orientation) { + if (r.inMultiWindowMode() || r.getRequestedConfigurationOrientation(true /* forDisplay */, + activityOrientation) == getConfiguration().orientation) { return ROTATION_UNDEFINED; } final int currentRotation = getRotation(); - final int rotation = mDisplayRotation.rotationForOrientation(r.getRequestedOrientation(), + final int rotation = mDisplayRotation.rotationForOrientation(activityOrientation, currentRotation); if (rotation == currentRotation) { return ROTATION_UNDEFINED; diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS index 781023c688c3..5d6d8bcc579f 100644 --- a/services/core/java/com/android/server/wm/OWNERS +++ b/services/core/java/com/android/server/wm/OWNERS @@ -24,6 +24,7 @@ pdwilliams@google.com per-file Background*Start* = set noparent per-file Background*Start* = file:/BAL_OWNERS per-file Background*Start* = ogunwale@google.com, louischang@google.com +per-file BackgroundLaunchProcessController.java = file:/BAL_OWNERS # File related to activity callers per-file ActivityCallerState.java = file:/core/java/android/app/COMPONENT_CALLER_OWNERS diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 6995027aac78..790ca1b74453 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1731,13 +1731,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * last time {@link #getOrientation(int) was called. */ @Nullable - WindowContainer getLastOrientationSource() { - final WindowContainer source = mLastOrientationSource; - if (source != null && source != this) { - final WindowContainer nextSource = source.getLastOrientationSource(); - if (nextSource != null) { - return nextSource; - } + final WindowContainer<?> getLastOrientationSource() { + if (mLastOrientationSource == null) { + return null; + } + WindowContainer<?> source = this; + while (source != source.mLastOrientationSource && source.mLastOrientationSource != null) { + source = source.mLastOrientationSource; } return source; } diff --git a/services/core/java/com/android/server/wm/WindowManagerConstants.java b/services/core/java/com/android/server/wm/WindowManagerConstants.java index 1931be4015c6..47c42f4292f1 100644 --- a/services/core/java/com/android/server/wm/WindowManagerConstants.java +++ b/services/core/java/com/android/server/wm/WindowManagerConstants.java @@ -34,6 +34,10 @@ import java.util.concurrent.Executor; */ final class WindowManagerConstants { + /** The orientation of activity will be always "unspecified". */ + private static final String KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST = + "ignore_activity_orientation_request"; + /** * The minimum duration between gesture exclusion logging for a given window in * milliseconds. @@ -58,6 +62,9 @@ final class WindowManagerConstants { /** @see AndroidDeviceConfig#KEY_SYSTEM_GESTURES_EXCLUDED_BY_PRE_Q_STICKY_IMMERSIVE */ boolean mSystemGestureExcludedByPreQStickyImmersive; + /** @see #KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST */ + boolean mIgnoreActivityOrientationRequest; + private final WindowManagerGlobalLock mGlobalLock; private final Runnable mUpdateSystemGestureExclusionCallback; private final DeviceConfigInterface mDeviceConfig; @@ -89,6 +96,7 @@ final class WindowManagerConstants { updateSystemGestureExclusionLogDebounceMillis(); updateSystemGestureExclusionLimitDp(); updateSystemGestureExcludedByPreQStickyImmersive(); + updateIgnoreActivityOrientationRequest(); } private void onAndroidPropertiesChanged(DeviceConfig.Properties properties) { @@ -127,6 +135,9 @@ final class WindowManagerConstants { case KEY_SYSTEM_GESTURE_EXCLUSION_LOG_DEBOUNCE_MILLIS: updateSystemGestureExclusionLogDebounceMillis(); break; + case KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST: + updateIgnoreActivityOrientationRequest(); + break; default: break; } @@ -152,6 +163,12 @@ final class WindowManagerConstants { KEY_SYSTEM_GESTURES_EXCLUDED_BY_PRE_Q_STICKY_IMMERSIVE, false); } + private void updateIgnoreActivityOrientationRequest() { + mIgnoreActivityOrientationRequest = mDeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, + KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST, false); + } + void dump(PrintWriter pw) { pw.println("WINDOW MANAGER CONSTANTS (dumpsys window constants):"); @@ -161,6 +178,8 @@ final class WindowManagerConstants { pw.print("="); pw.println(mSystemGestureExclusionLimitDp); pw.print(" "); pw.print(KEY_SYSTEM_GESTURES_EXCLUDED_BY_PRE_Q_STICKY_IMMERSIVE); pw.print("="); pw.println(mSystemGestureExcludedByPreQStickyImmersive); + pw.print(" "); pw.print(KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST); + pw.print("="); pw.println(mIgnoreActivityOrientationRequest); pw.println(); } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index e3ceb3348de7..29ab4dd79edc 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7920,7 +7920,7 @@ public class WindowManagerService extends IWindowManager.Stub } boolean allWindowsDrawn = false; synchronized (mGlobalLock) { - if ((displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) + if (displayId == INVALID_DISPLAY && mRoot.getDefaultDisplay().mDisplayUpdater.waitForTransition(message)) { // Use the ready-to-play of transition as the signal. return; diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index d96ebc6655ac..b6b36c716a53 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -89,6 +89,7 @@ import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.Watchdog; import com.android.server.grammaticalinflection.GrammaticalInflectionManagerInternal; import com.android.server.wm.ActivityTaskManagerService.HotPath; +import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration; import java.io.IOException; import java.io.PrintWriter; @@ -695,20 +696,13 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio public boolean areBackgroundFgsStartsAllowed() { return areBackgroundActivityStartsAllowed( mAtm.getBalAppSwitchesState(), - true /* isCheckingForFgsStart */).allows(); + BackgroundLaunchProcessController.CHECK_FOR_FGS_START).allows(); } BackgroundActivityStartController.BalVerdict areBackgroundActivityStartsAllowed( - int appSwitchState) { - return areBackgroundActivityStartsAllowed( - appSwitchState, - false /* isCheckingForFgsStart */); - } - - private BackgroundActivityStartController.BalVerdict areBackgroundActivityStartsAllowed( - int appSwitchState, boolean isCheckingForFgsStart) { + int appSwitchState, BalCheckConfiguration checkConfiguration) { return mBgLaunchController.areBackgroundActivityStartsAllowed(mPid, mUid, - mInfo.packageName, appSwitchState, isCheckingForFgsStart, + mInfo.packageName, appSwitchState, checkConfiguration, hasActivityInVisibleTask(), mInstrumentingWithBackgroundActivityStartPrivileges, mAtm.getLastStopAppSwitchesTime(), mLastActivityLaunchTime, mLastActivityFinishTime); diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index 4231149336ec..0eafb59bdeac 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -42,7 +42,7 @@ <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> - <xs:element type="thermalThrottling" name="thermalThrottling"> + <xs:element type="thermalThrottling" name="thermalThrottling" minOccurs="0" maxOccurs="1"> <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> @@ -464,7 +464,15 @@ <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> - <xs:element name="pollingWindowMillis" type="xs:nonNegativeInteger"> + <xs:element name="customAnimationRateSec" type="nonNegativeDecimal" minOccurs="0" maxOccurs="1"> + <xs:annotation name="nonnull"/> + <xs:annotation name="final"/> + </xs:element> + <xs:element name="pollingWindowMaxMillis" type="xs:nonNegativeInteger"> + <xs:annotation name="nonnull"/> + <xs:annotation name="final"/> + </xs:element> + <xs:element name="pollingWindowMinMillis" type="xs:nonNegativeInteger"> <xs:annotation name="nonnull"/> <xs:annotation name="final"/> </xs:element> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index cec2787ca51f..355b0ab15a62 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -345,10 +345,14 @@ package com.android.server.display.config { public class PowerThrottlingConfig { ctor public PowerThrottlingConfig(); method @NonNull public final java.math.BigDecimal getBrightnessLowestCapAllowed(); - method @NonNull public final java.math.BigInteger getPollingWindowMillis(); + method @NonNull public final java.math.BigDecimal getCustomAnimationRateSec(); + method @NonNull public final java.math.BigInteger getPollingWindowMaxMillis(); + method @NonNull public final java.math.BigInteger getPollingWindowMinMillis(); method public final java.util.List<com.android.server.display.config.PowerThrottlingMap> getPowerThrottlingMap(); method public final void setBrightnessLowestCapAllowed(@NonNull java.math.BigDecimal); - method public final void setPollingWindowMillis(@NonNull java.math.BigInteger); + method public final void setCustomAnimationRateSec(@NonNull java.math.BigDecimal); + method public final void setPollingWindowMaxMillis(@NonNull java.math.BigInteger); + method public final void setPollingWindowMinMillis(@NonNull java.math.BigInteger); } public class PowerThrottlingMap { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index 5eec0124a9e3..b982098fefa4 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -1325,11 +1325,6 @@ class ActiveAdmin { pw.print("encryptionRequested="); pw.println(encryptionRequested); - if (!Flags.policyEngineMigrationV2Enabled()) { - pw.print("mUsbDataSignaling="); - pw.println(mUsbDataSignalingEnabled); - } - pw.print("disableCallerId="); pw.println(disableCallerId); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index a08af72586ee..4beb6a8a3480 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -230,11 +230,9 @@ final class DevicePolicyEngine { synchronized (mLock) { PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - if (!handleAdminPolicySizeLimit(localPolicyState, enforcingAdmin, value, - policyDefinition, userId)) { - return; - } + if (!handleAdminPolicySizeLimit(localPolicyState, enforcingAdmin, value, + policyDefinition, userId)) { + return; } if (policyDefinition.isNonCoexistablePolicy()) { @@ -354,9 +352,7 @@ final class DevicePolicyEngine { } PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - decreasePolicySizeForAdmin(localPolicyState, enforcingAdmin); - } + decreasePolicySizeForAdmin(localPolicyState, enforcingAdmin); if (policyDefinition.isNonCoexistablePolicy()) { setNonCoexistableLocalPolicyLocked(policyDefinition, localPolicyState, @@ -500,11 +496,9 @@ final class DevicePolicyEngine { synchronized (mLock) { PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - if (!handleAdminPolicySizeLimit(globalPolicyState, enforcingAdmin, value, - policyDefinition, UserHandle.USER_ALL)) { - return; - } + if (!handleAdminPolicySizeLimit(globalPolicyState, enforcingAdmin, value, + policyDefinition, UserHandle.USER_ALL)) { + return; } // TODO(b/270999567): Move error handling for DISALLOW_CELLULAR_2G into the code // that honors the restriction once there's an API available @@ -571,9 +565,7 @@ final class DevicePolicyEngine { synchronized (mLock) { PolicyState<V> policyState = getGlobalPolicyStateLocked(policyDefinition); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - decreasePolicySizeForAdmin(policyState, enforcingAdmin); - } + decreasePolicySizeForAdmin(policyState, enforcingAdmin); boolean policyChanged = policyState.removePolicy(enforcingAdmin); @@ -1739,25 +1731,23 @@ final class DevicePolicyEngine { pw.println(); } pw.decreaseIndent(); - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - pw.println(); + pw.println(); - pw.println("Default admin policy size limit: " + DEFAULT_POLICY_SIZE_LIMIT); - pw.println("Current admin policy size limit: " + mPolicySizeLimit); - pw.println("Admin Policies size: "); - for (int i = 0; i < mAdminPolicySize.size(); i++) { - int userId = mAdminPolicySize.keyAt(i); - pw.printf("User %d:\n", userId); - pw.increaseIndent(); - for (EnforcingAdmin admin : mAdminPolicySize.get(userId).keySet()) { - pw.printf("Admin : " + admin + " : " + mAdminPolicySize.get(userId).get( - admin)); - pw.println(); - } - pw.decreaseIndent(); + pw.println("Default admin policy size limit: " + DEFAULT_POLICY_SIZE_LIMIT); + pw.println("Current admin policy size limit: " + mPolicySizeLimit); + pw.println("Admin Policies size: "); + for (int i = 0; i < mAdminPolicySize.size(); i++) { + int userId = mAdminPolicySize.keyAt(i); + pw.printf("User %d:\n", userId); + pw.increaseIndent(); + for (EnforcingAdmin admin : mAdminPolicySize.get(userId).keySet()) { + pw.printf("Admin : " + admin + " : " + mAdminPolicySize.get(userId).get( + admin)); + pw.println(); } pw.decreaseIndent(); } + pw.decreaseIndent(); } } @@ -2018,23 +2008,21 @@ final class DevicePolicyEngine { private void writeEnforcingAdminSizeInner(TypedXmlSerializer serializer) throws IOException { - if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - if (mAdminPolicySize != null) { - for (int i = 0; i < mAdminPolicySize.size(); i++) { - int userId = mAdminPolicySize.keyAt(i); - for (EnforcingAdmin admin : mAdminPolicySize.get( - userId).keySet()) { - serializer.startTag(/* namespace= */ null, - TAG_ENFORCING_ADMIN_AND_SIZE); - serializer.startTag(/* namespace= */ null, TAG_ENFORCING_ADMIN); - admin.saveToXml(serializer); - serializer.endTag(/* namespace= */ null, TAG_ENFORCING_ADMIN); - serializer.startTag(/* namespace= */ null, TAG_POLICY_SUM_SIZE); - serializer.attributeInt(/* namespace= */ null, ATTR_POLICY_SUM_SIZE, - mAdminPolicySize.get(userId).get(admin)); - serializer.endTag(/* namespace= */ null, TAG_POLICY_SUM_SIZE); - serializer.endTag(/* namespace= */ null, TAG_ENFORCING_ADMIN_AND_SIZE); - } + if (mAdminPolicySize != null) { + for (int i = 0; i < mAdminPolicySize.size(); i++) { + int userId = mAdminPolicySize.keyAt(i); + for (EnforcingAdmin admin : mAdminPolicySize.get( + userId).keySet()) { + serializer.startTag(/* namespace= */ null, + TAG_ENFORCING_ADMIN_AND_SIZE); + serializer.startTag(/* namespace= */ null, TAG_ENFORCING_ADMIN); + admin.saveToXml(serializer); + serializer.endTag(/* namespace= */ null, TAG_ENFORCING_ADMIN); + serializer.startTag(/* namespace= */ null, TAG_POLICY_SUM_SIZE); + serializer.attributeInt(/* namespace= */ null, ATTR_POLICY_SUM_SIZE, + mAdminPolicySize.get(userId).get(admin)); + serializer.endTag(/* namespace= */ null, TAG_POLICY_SUM_SIZE); + serializer.endTag(/* namespace= */ null, TAG_ENFORCING_ADMIN_AND_SIZE); } } } @@ -2042,9 +2030,6 @@ final class DevicePolicyEngine { private void writeMaxPolicySizeInner(TypedXmlSerializer serializer) throws IOException { - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - return; - } serializer.startTag(/* namespace= */ null, TAG_MAX_POLICY_SIZE_LIMIT); serializer.attributeInt( /* namespace= */ null, ATTR_POLICY_SUM_SIZE, mPolicySizeLimit); @@ -2192,9 +2177,6 @@ final class DevicePolicyEngine { private void readMaxPolicySizeInner(TypedXmlPullParser parser) throws XmlPullParserException, IOException { - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - return; - } mPolicySizeLimit = parser.getAttributeInt(/* namespace= */ null, ATTR_POLICY_SUM_SIZE); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 886ae7ad7e50..fc619677bb56 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1328,9 +1328,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Bundle prevRestrictions) { resetCrossProfileIntentFiltersIfNeeded(userId, newRestrictions, prevRestrictions); resetUserVpnIfNeeded(userId, newRestrictions, prevRestrictions); - if (Flags.deletePrivateSpaceUnderRestriction()) { - removePrivateSpaceIfRestrictionIsSet(userId, newRestrictions, prevRestrictions); - } + removePrivateSpaceIfRestrictionIsSet(userId, newRestrictions, prevRestrictions); } private void resetUserVpnIfNeeded( @@ -3695,9 +3693,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } revertTransferOwnershipIfNecessaryLocked(); - if (!Flags.policyEngineMigrationV2Enabled()) { - updateUsbDataSignal(mContext, isUsbDataSignalingEnabledInternalLocked()); - } } // Check whether work apps were paused via suspension and unsuspend if necessary. @@ -9377,8 +9372,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { void sendDeviceOwnerOrProfileOwnerCommand(String action, Bundle extras, int userId) { if (userId == UserHandle.USER_ALL) { - if (Flags.headlessDeviceOwnerDelegateSecurityLoggingBugFix() - && getHeadlessDeviceOwnerModeForDeviceOwner() + if (getHeadlessDeviceOwnerModeForDeviceOwner() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER) { userId = mOwners.getDeviceOwnerUserId(); } else { @@ -12452,12 +12446,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } if (packageList != null) { - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - for (String pkg : packageList) { - PolicySizeVerifier.enforceMaxPackageNameLength(pkg); - } - } - List<InputMethodInfo> enabledImes = mInjector.binderWithCleanCallingIdentity(() -> InputMethodManagerInternal.get().getEnabledInputMethodListAsUser(userId)); if (enabledImes != null) { @@ -14320,10 +14308,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return; } - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - PolicySizeVerifier.enforceMaxStringLength(accountType, "account type"); - } - CallerIdentity caller = getCallerIdentity(who, callerPackageName); synchronized (getLockObject()) { int affectedUser = getAffectedUser(parent); @@ -14934,11 +14918,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { public void setLockTaskPackages(ComponentName who, String callerPackageName, String[] packages) throws SecurityException { Objects.requireNonNull(packages, "packages is null"); - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - for (String pkg : packages) { - PolicySizeVerifier.enforceMaxPackageNameLength(pkg); - } - } CallerIdentity caller = getCallerIdentity(who, callerPackageName); checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_LOCK_TASK_PACKAGES); @@ -15219,7 +15198,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(who); Preconditions.checkCallAuthorization( isProfileOwner(caller) || isDefaultDeviceOwner(caller)); - if (Flags.allowScreenBrightnessControlOnCope() && parent) { + if (parent) { Preconditions.checkCallAuthorization(isProfileOwnerOfOrganizationOwnedDevice(caller)); } checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_SYSTEM_SETTING); @@ -15230,7 +15209,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { "Permission denial: device owners cannot update %1$s", setting)); } int affectedUser; - if (Flags.allowScreenBrightnessControlOnCope() && parent) { + if (parent) { affectedUser = getProfileParentId(caller.getUserId()); } else { affectedUser = caller.getUserId(); @@ -16822,13 +16801,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); } - if (Flags.permissionMigrationForZeroTrustImplEnabled()) { - final UserHandle user = UserHandle.of(userId); - final String roleHolderPackage = getRoleHolderPackageNameOnUser( - RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT, userId); - if (roleHolderPackage != null) { - broadcastExplicitIntentToPackage(intent, roleHolderPackage, user); - } + final UserHandle user = UserHandle.of(userId); + final String roleHolderPackage = getRoleHolderPackageNameOnUser( + RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT, userId); + if (roleHolderPackage != null) { + broadcastExplicitIntentToPackage(intent, roleHolderPackage, user); } } }); @@ -16836,18 +16813,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public SystemUpdateInfo getPendingSystemUpdate(ComponentName admin, String callerPackage) { - if (Flags.permissionMigrationForZeroTrustImplEnabled()) { - CallerIdentity caller = getCallerIdentity(admin, callerPackage); - enforcePermissions(new String[] {NOTIFY_PENDING_SYSTEM_UPDATE, - MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES}, caller.getPackageName(), - caller.getUserId()); - } else { - Objects.requireNonNull(admin, "ComponentName is null"); - - final CallerIdentity caller = getCallerIdentity(admin); - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isProfileOwner(caller)); - } + CallerIdentity caller = getCallerIdentity(admin, callerPackage); + enforcePermissions(new String[] {NOTIFY_PENDING_SYSTEM_UPDATE, + MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES}, caller.getPackageName(), + caller.getUserId()); return mOwners.getSystemUpdateInfo(); } @@ -17391,17 +17360,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Nullable ComponentName componentName, @UserIdInt int callingUserId) { synchronized (getLockObject()) { int deviceOwnerUserId = -1; - if (Flags.headlessDeviceOwnerProvisioningFixEnabled()) { - deviceOwnerUserId = mInjector.userManagerIsHeadlessSystemUserMode() - && getHeadlessDeviceOwnerModeForDeviceAdmin(componentName, callingUserId) - == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED - ? UserHandle.USER_SYSTEM : callingUserId; - } else { - deviceOwnerUserId = mInjector.userManagerIsHeadlessSystemUserMode() - && getHeadlessDeviceOwnerModeForDeviceOwner() - == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED - ? UserHandle.USER_SYSTEM : callingUserId; - } + deviceOwnerUserId = mInjector.userManagerIsHeadlessSystemUserMode() + && getHeadlessDeviceOwnerModeForDeviceAdmin(componentName, callingUserId) + == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED + ? UserHandle.USER_SYSTEM : callingUserId; Slogf.i(LOG_TAG, "Calling user %d, device owner will be set on user %d", callingUserId, deviceOwnerUserId); // hasIncompatibleAccountsOrNonAdb doesn't matter since the caller is not adb. @@ -21442,13 +21404,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(callerPackage); - if (Flags.permissionMigrationForZeroTrustImplEnabled()) { - enforcePermission(MANAGE_DEVICE_POLICY_CERTIFICATES, caller.getPackageName()); - } else { - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isProfileOwner(caller) - || isCallerDelegate(caller, DELEGATION_CERT_INSTALL)); - } + enforcePermission(MANAGE_DEVICE_POLICY_CERTIFICATES, caller.getPackageName()); synchronized (getLockObject()) { final ActiveAdmin requiredAdmin = getDeviceOrProfileOwnerAdminLocked( caller.getUserId()); @@ -22047,16 +22003,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final long identity = Binder.clearCallingIdentity(); try { boolean isSingleUserMode; - if (Flags.headlessDeviceOwnerProvisioningFixEnabled()) { - int headlessDeviceOwnerMode = getHeadlessDeviceOwnerModeForDeviceAdmin( - deviceAdmin, caller.getUserId()); - isSingleUserMode = - headlessDeviceOwnerMode == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; - } else { - isSingleUserMode = - getHeadlessDeviceOwnerModeForDeviceOwner() - == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; - } + int headlessDeviceOwnerMode = getHeadlessDeviceOwnerModeForDeviceAdmin( + deviceAdmin, caller.getUserId()); + isSingleUserMode = headlessDeviceOwnerMode == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; if (Flags.headlessSingleMinTargetSdk() && mInjector.userManagerIsHeadlessSystemUserMode() @@ -22455,35 +22404,17 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Objects.requireNonNull(packageName, "Admin package name must be provided"); final CallerIdentity caller = getCallerIdentity(packageName); - if (!Flags.policyEngineMigrationV2Enabled()) { - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isProfileOwnerOfOrganizationOwnedDevice(caller), - "USB data signaling can only be controlled by a device owner or " - + "a profile owner on an organization-owned device."); + synchronized (getLockObject()) { + EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( + /* admin= */ null, MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING, + caller.getPackageName(), + caller.getUserId()); Preconditions.checkState(canUsbDataSignalingBeDisabled(), "USB data signaling cannot be disabled."); - } - - synchronized (getLockObject()) { - if (Flags.policyEngineMigrationV2Enabled()) { - EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( - /* admin= */ null, MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING, - caller.getPackageName(), - caller.getUserId()); - Preconditions.checkState(canUsbDataSignalingBeDisabled(), - "USB data signaling cannot be disabled."); - mDevicePolicyEngine.setGlobalPolicy( - PolicyDefinition.USB_DATA_SIGNALING, - enforcingAdmin, - new BooleanPolicyValue(enabled)); - } else { - ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(caller.getUserId()); - if (admin.mUsbDataSignalingEnabled != enabled) { - admin.mUsbDataSignalingEnabled = enabled; - saveSettingsLocked(caller.getUserId()); - updateUsbDataSignal(mContext, isUsbDataSignalingEnabledInternalLocked()); - } - } + mDevicePolicyEngine.setGlobalPolicy( + PolicyDefinition.USB_DATA_SIGNALING, + enforcingAdmin, + new BooleanPolicyValue(enabled)); } DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_USB_DATA_SIGNALING) @@ -22505,24 +22436,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public boolean isUsbDataSignalingEnabled(String packageName) { final CallerIdentity caller = getCallerIdentity(packageName); - if (Flags.policyEngineMigrationV2Enabled()) { - Boolean enabled = mDevicePolicyEngine.getResolvedPolicy( - PolicyDefinition.USB_DATA_SIGNALING, - caller.getUserId()); - return enabled == null || enabled; - } else { - synchronized (getLockObject()) { - // If the caller is an admin, return the policy set by itself. Otherwise - // return the device-wide policy. - if (isDefaultDeviceOwner(caller) || isProfileOwnerOfOrganizationOwnedDevice( - caller)) { - return getProfileOwnerOrDeviceOwnerLocked( - caller.getUserId()).mUsbDataSignalingEnabled; - } else { - return isUsbDataSignalingEnabledInternalLocked(); - } - } - } + Boolean enabled = mDevicePolicyEngine.getResolvedPolicy( + PolicyDefinition.USB_DATA_SIGNALING, + caller.getUserId()); + return enabled == null || enabled; } private boolean isUsbDataSignalingEnabledInternalLocked() { @@ -24875,9 +24792,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void setMaxPolicyStorageLimit(String callerPackageName, int storageLimit) { - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - return; - } CallerIdentity caller = getCallerIdentity(callerPackageName); enforcePermission(MANAGE_PROFILE_AND_DEVICE_OWNERS, caller.getPackageName(), caller.getUserId()); @@ -24891,9 +24805,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public int getMaxPolicyStorageLimit(String callerPackageName) { - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - return -1; - } CallerIdentity caller = getCallerIdentity(callerPackageName); enforcePermission(MANAGE_PROFILE_AND_DEVICE_OWNERS, caller.getPackageName(), caller.getUserId()); @@ -24903,9 +24814,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void forceSetMaxPolicyStorageLimit(String callerPackageName, int storageLimit) { - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - return; - } CallerIdentity caller = getCallerIdentity(callerPackageName); enforcePermission(MANAGE_DEVICE_POLICY_STORAGE_LIMIT, caller.getPackageName(), caller.getUserId()); @@ -24916,9 +24824,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public int getPolicySizeForAdmin( String callerPackageName, android.app.admin.EnforcingAdmin admin) { - if (!Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { - return -1; - } CallerIdentity caller = getCallerIdentity(callerPackageName); enforcePermission(MANAGE_DEVICE_POLICY_STORAGE_LIMIT, caller.getPackageName(), caller.getUserId()); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c5c371ff85d5..ab459df1cdf6 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -16,7 +16,6 @@ package com.android.server; -import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_HIGH; @@ -38,6 +37,7 @@ import android.app.ApplicationErrorReport; import android.app.INotificationManager; import android.app.SystemServiceRegistry; import android.app.admin.DevicePolicySafetyChecker; +import android.app.appfunctions.AppFunctionManagerConfiguration; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.ContentResolver; @@ -1743,12 +1743,11 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(LogcatManagerService.class); t.traceEnd(); - t.traceBegin("StartAppFunctionManager"); - if (enableAppFunctionManager()) { + if (AppFunctionManagerConfiguration.isSupported(context)) { + t.traceBegin("StartAppFunctionManager"); mSystemServiceManager.startService(AppFunctionManagerService.class); + t.traceEnd(); } - t.traceEnd(); - } catch (Throwable e) { Slog.e("System", "******************************************"); Slog.e("System", "************ Failure starting core service"); @@ -2468,8 +2467,8 @@ public final class SystemServer implements Dumpable { reportWtf("starting RuntimeService", e); } t.traceEnd(); - - if (!isWatch && !disableNetworkTime) { + if (!disableNetworkTime && (!isWatch || (isWatch + && android.server.Flags.allowNetworkTimeUpdateService()))) { t.traceBegin("StartNetworkTimeUpdateService"); try { networkTimeUpdater = new NetworkTimeUpdateService(context); diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index 29f387117073..ec74ef191b81 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -28,4 +28,11 @@ flag { namespace: "wear_frameworks" description: "Allow removing VpnManagerService" bug: "340928692" +} + +flag { + name: "allow_network_time_update_service" + namespace: "wear_systems" + description: "Allow NetworkTimeUpdateService on Wear" + bug: "327508176" }
\ No newline at end of file diff --git a/services/supervision/OWNERS b/services/supervision/OWNERS new file mode 100644 index 000000000000..e5f41472df7b --- /dev/null +++ b/services/supervision/OWNERS @@ -0,0 +1 @@ +include /core/java/android/app/supervision/OWNERS diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index a4ef629492e7..7ffd0eca9b96 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -20,7 +20,9 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.supervision.ISupervisionManager; import android.content.Context; - +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; import com.android.internal.util.DumpUtils; import com.android.server.SystemService; @@ -28,7 +30,9 @@ import com.android.server.SystemService; import java.io.FileDescriptor; import java.io.PrintWriter; -/** Service for handling system supervision. */ +/** + * Service for handling system supervision. + */ public class SupervisionService extends ISupervisionManager.Stub { private static final String LOG_TAG = "SupervisionService"; @@ -44,8 +48,20 @@ public class SupervisionService extends ISupervisionManager.Stub { } @Override - protected void dump(@NonNull FileDescriptor fd, - @NonNull PrintWriter fout, @Nullable String[] args) { + public void onShellCommand( + @Nullable FileDescriptor in, + @Nullable FileDescriptor out, + @Nullable FileDescriptor err, + @NonNull String[] args, + @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) throws RemoteException { + new SupervisionServiceShellCommand(this) + .exec(this, in, out, err, args, callback, resultReceiver); + } + + @Override + protected void dump( + @NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return; fout.println("Supervision enabled: " + isSupervisionEnabled()); diff --git a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java new file mode 100644 index 000000000000..3aba24a3d4a5 --- /dev/null +++ b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 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.supervision; + +import android.os.ShellCommand; + +import java.io.PrintWriter; + +public class SupervisionServiceShellCommand extends ShellCommand { + private final SupervisionService mService; + + public SupervisionServiceShellCommand(SupervisionService mService) { + this.mService = mService; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(null); + } + final PrintWriter pw = getOutPrintWriter(); + switch (cmd) { + case "help": return help(pw); + case "is-enabled": return isEnabled(pw); + default: return handleDefaultCommands(cmd); + } + } + + private int help(PrintWriter pw) { + pw.println("Supervision service commands:"); + pw.println(" help"); + pw.println(" Prints this help text"); + pw.println(" is-enabled"); + pw.println(" Is supervision enabled"); + return 0; + } + + private int isEnabled(PrintWriter pw) { + pw.println(mService.isSupervisionEnabled()); + return 0; + } + + @Override + public void onHelp() { + help(getOutPrintWriter()); + } +} diff --git a/services/tests/appfunctions/OWNERS b/services/tests/appfunctions/OWNERS new file mode 100644 index 000000000000..7fa891736efe --- /dev/null +++ b/services/tests/appfunctions/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 1627156 +include platform/frameworks/base:/core/java/android/app/appfunctions/OWNERS diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java index f690b1bbfccf..2d4a29b3ed6f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java @@ -18,6 +18,8 @@ package com.android.server.display; import static org.junit.Assert.assertEquals; +import android.hardware.display.BrightnessInfo; + import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -112,7 +114,10 @@ public class DisplayBrightnessStateTest { .append("\n mBrightnessAdjustmentFlag:") .append(displayBrightnessState.getBrightnessAdjustmentFlag()) .append("\n mIsUserInitiatedChange:") - .append(displayBrightnessState.isUserInitiatedChange()); + .append(displayBrightnessState.isUserInitiatedChange()) + .append("\n mBrightnessMaxReason:") + .append(BrightnessInfo.briMaxReasonToString( + displayBrightnessState.getBrightnessMaxReason())); return sb.toString(); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index d4506831d9c2..fd05b26c320b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -263,7 +263,9 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getPowerThrottlingConfigData(); assertNotNull(powerThrottlingConfigData); assertEquals(0.1f, powerThrottlingConfigData.brightnessLowestCapAllowed, SMALL_DELTA); - assertEquals(10, powerThrottlingConfigData.pollingWindowMillis); + assertEquals(15f, powerThrottlingConfigData.customAnimationRateSec, SMALL_DELTA); + assertEquals(20000, powerThrottlingConfigData.pollingWindowMaxMillis); + assertEquals(10000, powerThrottlingConfigData.pollingWindowMinMillis); } @Test @@ -1295,7 +1297,9 @@ public final class DisplayDeviceConfigTest { private String getPowerThrottlingConfig() { return "<powerThrottlingConfig >\n" + "<brightnessLowestCapAllowed>0.1</brightnessLowestCapAllowed>\n" - + "<pollingWindowMillis>10</pollingWindowMillis>\n" + + "<customAnimationRateSec>15</customAnimationRateSec>\n" + + "<pollingWindowMaxMillis>20000</pollingWindowMaxMillis>\n" + + "<pollingWindowMinMillis>10000</pollingWindowMinMillis>\n" + "<powerThrottlingMap>\n" + "<powerThrottlingPoint>\n" + "<thermalStatus>light</thermalStatus>\n" diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 2166cb7639ef..d0aec3b6cef8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -2587,7 +2587,7 @@ public final class DisplayPowerControllerTest { BrightnessClamperController getBrightnessClamperController(Handler handler, BrightnessClamperController.ClamperChangeListener clamperChangeListener, BrightnessClamperController.DisplayDeviceData data, Context context, - DisplayManagerFlags flags, SensorManager sensorManager) { + DisplayManagerFlags flags, SensorManager sensorManager, float currentBrightness) { return mClamperController; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index 0ce9233b4b6f..f9dc12258667 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.hardware.SensorManager; -import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManagerInternal; import android.os.Handler; import android.os.PowerManager; @@ -161,12 +160,6 @@ public class BrightnessClamperControllerTest { } @Test - public void testMaxReasonIsNoneOnInit() { - assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, - mClamperController.getBrightnessMaxReason()); - } - - @Test public void testOnDisplayChanged_DelegatesToClamper() { mClamperController.onDisplayChanged(mMockDisplayDeviceData); @@ -365,7 +358,7 @@ public class BrightnessClamperControllerTest { private BrightnessClamperController createBrightnessClamperController() { return new BrightnessClamperController(mTestInjector, mTestHandler, mMockExternalListener, - mMockDisplayDeviceData, mMockContext, mFlags, mSensorManager); + mMockDisplayDeviceData, mMockContext, mFlags, mSensorManager, 0); } interface TestDisplayListenerModifier extends BrightnessStateModifier, @@ -403,7 +396,7 @@ public class BrightnessClamperControllerTest { Handler handler, BrightnessClamperController.ClamperChangeListener clamperChangeListener, BrightnessClamperController.DisplayDeviceData data, - DisplayManagerFlags flags, Context context) { + DisplayManagerFlags flags, Context context, float currentBrightness) { mCapturedChangeListener = clamperChangeListener; return mClampers; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessPowerClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessPowerClamperTest.java index b3f33ad858fe..c4898da62d81 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessPowerClamperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessPowerClamperTest.java @@ -21,6 +21,7 @@ import static com.android.server.display.brightness.clamper.BrightnessPowerClamp import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import android.os.IThermalService; import android.os.PowerManager; import android.os.RemoteException; import android.os.Temperature; @@ -58,12 +59,18 @@ public class BrightnessPowerClamperTest { private final FakeDeviceConfigInterface mFakeDeviceConfigInterface = new FakeDeviceConfigInterface(); private final TestHandler mTestHandler = new TestHandler(null); + private final TestInjector mTestInjector = new TestInjector(); private BrightnessPowerClamper mClamper; + private final float mCurrentBrightness = 0.6f; + private PowerChangeListener mPowerChangeListener; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mClamper = new BrightnessPowerClamper(new TestInjector(), mTestHandler, - mMockClamperChangeListener, new TestPowerData()); + mClamper = new BrightnessPowerClamper(mTestInjector, mTestHandler, + mMockClamperChangeListener, new TestPowerData(), mCurrentBrightness); + mPowerChangeListener = mClamper.getPowerChangeListener(); + mPmicMonitor = mTestInjector.getPmicMonitor(mPowerChangeListener, null, 5, 10); + mPmicMonitor.setPowerChangeListener(mPowerChangeListener); mTestHandler.flush(); } @@ -79,36 +86,27 @@ public class BrightnessPowerClamperTest { } @Test - public void testPowerThrottlingNoOngoingAnimation() throws RemoteException { - mPmicMonitor.setThermalStatus(Temperature.THROTTLING_SEVERE); + public void testPowerThrottlingWithThermalLevelLight() throws RemoteException { + mPmicMonitor.setThermalStatus(Temperature.THROTTLING_LIGHT); mTestHandler.flush(); assertFalse(mClamper.isActive()); assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); // update a new device config for power-throttling. mClamper.onDisplayChanged(new TestPowerData( - List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, 100f)))); + List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_LIGHT, 100f)))); mPmicMonitor.setAvgPowerConsumed(200f); float expectedBrightness = 0.5f; - expectedBrightness = expectedBrightness * PowerManager.BRIGHTNESS_MAX; + expectedBrightness = expectedBrightness * mCurrentBrightness; mTestHandler.flush(); // Assume current brightness as max, as there is no throttling. assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); - mPmicMonitor.setThermalStatus(Temperature.THROTTLING_CRITICAL); - // update a new device config for power-throttling. - mClamper.onDisplayChanged(new TestPowerData( - List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 50f)))); - - mPmicMonitor.setAvgPowerConsumed(100f); - expectedBrightness = 0.5f * PowerManager.BRIGHTNESS_MAX; - mTestHandler.flush(); - assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); } @Test - public void testPowerThrottlingWithOngoingAnimation() throws RemoteException { + public void testPowerThrottlingWithThermalLevelSevere() throws RemoteException { mPmicMonitor.setThermalStatus(Temperature.THROTTLING_SEVERE); mTestHandler.flush(); assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); @@ -119,20 +117,10 @@ public class BrightnessPowerClamperTest { mPmicMonitor.setAvgPowerConsumed(200f); float expectedBrightness = 0.5f; - expectedBrightness = expectedBrightness * PowerManager.BRIGHTNESS_MAX; - + expectedBrightness = expectedBrightness * mCurrentBrightness; mTestHandler.flush(); // Assume current brightness as max, as there is no throttling. assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); - mPmicMonitor.setThermalStatus(Temperature.THROTTLING_CRITICAL); - // update a new device config for power-throttling. - mClamper.onDisplayChanged(new TestPowerData( - List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL, 50f)))); - - mPmicMonitor.setAvgPowerConsumed(100f); - expectedBrightness = 0.5f * PowerManager.BRIGHTNESS_MAX; - mTestHandler.flush(); - assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); } @Test @@ -148,8 +136,7 @@ public class BrightnessPowerClamperTest { mPmicMonitor.setAvgPowerConsumed(200f); float expectedBrightness = 0.5f; - expectedBrightness = expectedBrightness * PowerManager.BRIGHTNESS_MAX; - + expectedBrightness = expectedBrightness * mCurrentBrightness; mTestHandler.flush(); assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); @@ -169,10 +156,11 @@ public class BrightnessPowerClamperTest { private static class TestPmicMonitor extends PmicMonitor { private Temperature mCurrentTemperature; - private final PowerChangeListener mListener; - TestPmicMonitor(PowerChangeListener listener, int pollingTime) { - super(listener, pollingTime); - mListener = listener; + private PowerChangeListener mListener; + TestPmicMonitor(PowerChangeListener listener, + IThermalService thermalService, + int pollingTimeMax, int pollingTimeMin) { + super(listener, thermalService, pollingTimeMax, pollingTimeMin); } public void setAvgPowerConsumed(float power) { int status = mCurrentTemperature.getStatus(); @@ -181,13 +169,18 @@ public class BrightnessPowerClamperTest { public void setThermalStatus(@Temperature.ThrottlingStatus int status) { mCurrentTemperature = new Temperature(100, Temperature.TYPE_SKIN, "test_temp", status); } + public void setPowerChangeListener(PowerChangeListener listener) { + mListener = listener; + } } private class TestInjector extends BrightnessPowerClamper.Injector { @Override TestPmicMonitor getPmicMonitor(PowerChangeListener listener, - int pollingTime) { - mPmicMonitor = new TestPmicMonitor(listener, pollingTime); + IThermalService thermalService, + int minPollingTimeMillis, int maxPollingTimeMillis) { + mPmicMonitor = new TestPmicMonitor(listener, thermalService, maxPollingTimeMillis, + minPollingTimeMillis); return mPmicMonitor; } @@ -216,7 +209,7 @@ public class BrightnessPowerClamperTest { mUniqueDisplayId = uniqueDisplayId; mDataId = dataId; mData = PowerThrottlingData.create(data); - mConfigData = new PowerThrottlingConfigData(0.1f, 10); + mConfigData = new PowerThrottlingConfigData(0.1f, 10, 20, 10); } @NonNull diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt index 34c6ba90a0ba..1f3f19fa3ea8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt @@ -50,7 +50,7 @@ class AppRequestObserverTest { } @Test - fun `test app request votes`(@TestParameter testCase: AppRequestTestCase) { + fun testAppRequestVotes(@TestParameter testCase: AppRequestTestCase) { whenever(mockFlags.ignoreAppPreferredRefreshRateRequest()) .thenReturn(testCase.ignoreRefreshRateRequest) val displayModeDirector = DisplayModeDirector( diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt index bf2edfed03dc..38412114a157 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt @@ -39,7 +39,7 @@ class BaseModeRefreshRateVoteTest { } @Test - fun `updates summary with base mode refresh rate if not set`() { + fun updatesSummary_doesNotUpdateSummary_baseModeRefreshRateNotSet() { val summary = createVotesSummary() baseModeVote.updateSummary(summary) @@ -48,7 +48,7 @@ class BaseModeRefreshRateVoteTest { } @Test - fun `keeps summary base mode refresh rate if set`() { + fun doesNotUpdateSummary_baseModeRefreshRateSet() { val summary = createVotesSummary() summary.appRequestBaseModeRefreshRate = OTHER_BASE_REFRESH_RATE @@ -58,7 +58,7 @@ class BaseModeRefreshRateVoteTest { } @Test - fun `keeps summary with base mode refresh rate if vote refresh rate is negative`() { + fun doesNotUpdateSummary_baseModeRefreshRateNotSet_requestedRefreshRateInvalid() { val invalidBaseModeVote = BaseModeRefreshRateVote(-10f) val summary = createVotesSummary() diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt index 209e5a30d4ff..0a3c28581ff9 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt @@ -45,7 +45,7 @@ class CombinedVoteTest { } @Test - fun `delegates update to children`() { + fun delegatesUpdateToChildren() { val summary = createVotesSummary() combinedVote.updateSummary(summary) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt index 38782c21fd69..5b5ae65db3e3 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt @@ -28,7 +28,7 @@ import org.junit.runner.RunWith class DisableRefreshRateSwitchingVoteTest { @Test - fun `disabled refresh rate switching is not changed`( + fun testDisableRefreshRateSwitch_alreadyDisabled( @TestParameter voteDisableSwitching: Boolean ) { val summary = createVotesSummary() @@ -41,7 +41,7 @@ class DisableRefreshRateSwitchingVoteTest { } @Test - fun `disables refresh rate switching if requested`() { + fun disablesRefreshRateSwitch_notDisabled_requested() { val summary = createVotesSummary() val vote = DisableRefreshRateSwitchingVote(true) @@ -51,7 +51,7 @@ class DisableRefreshRateSwitchingVoteTest { } @Test - fun `does not disable refresh rate switching if not requested`() { + fun doesNotDisableRefreshRateSwitch_notDisabled_notRequested() { val summary = createVotesSummary() val vote = DisableRefreshRateSwitchingVote(false) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt index 9edcc328e53e..0968edb79e02 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt @@ -37,7 +37,7 @@ class PhysicalVoteTest { } @Test - fun `updates minPhysicalRefreshRate if summary has less`() { + fun updatesMinPhysicalRefreshRateWithBiggerValue() { val summary = createVotesSummary() summary.minPhysicalRefreshRate = 45f @@ -47,7 +47,7 @@ class PhysicalVoteTest { } @Test - fun `does not update minPhysicalRefreshRate if summary has more`() { + fun doesNotUpdateMinPhysicalRefreshRateWithSmallerValue() { val summary = createVotesSummary() summary.minPhysicalRefreshRate = 75f @@ -57,7 +57,7 @@ class PhysicalVoteTest { } @Test - fun `updates maxPhysicalRefreshRate if summary has more`() { + fun updatesMaxPhysicalRefreshRateWithSmallerValue() { val summary = createVotesSummary() summary.maxPhysicalRefreshRate = 120f @@ -67,7 +67,7 @@ class PhysicalVoteTest { } @Test - fun `does not update maxPhysicalRefreshRate if summary has less`() { + fun doesNotUpdateMaxPhysicalRefreshRateWithBiggerValue() { val summary = createVotesSummary() summary.maxPhysicalRefreshRate = 75f @@ -77,7 +77,7 @@ class PhysicalVoteTest { } @Test - fun `updates maxRenderFrameRate if summary has more`() { + fun updatesMaxRenderFrameRateWithSmallerValue() { val summary = createVotesSummary() summary.maxRenderFrameRate = 120f @@ -87,7 +87,7 @@ class PhysicalVoteTest { } @Test - fun `does not update maxRenderFrameRate if summary has less`() { + fun doesNotUpdateMaxRenderFrameRateWithBiggerValue() { val summary = createVotesSummary() summary.maxRenderFrameRate = 75f diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt index 2d65f1c2c45e..9fa1e1b0cf22 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt @@ -38,7 +38,7 @@ class RenderVoteTest { } @Test - fun `updates minRenderFrameRate if summary has less`() { + fun updatesMinRenderFrameRateWithBiggerValue() { val summary = createVotesSummary() summary.minRenderFrameRate = 45f @@ -48,7 +48,7 @@ class RenderVoteTest { } @Test - fun `does not update minRenderFrameRate if summary has more`() { + fun doesNotUpdateMinRenderFrameRateWithSmallerValue() { val summary = createVotesSummary() summary.minRenderFrameRate = 75f @@ -58,7 +58,7 @@ class RenderVoteTest { } @Test - fun `updates maxRenderFrameRate if summary has more`() { + fun updatesMaxPRenderFrameRateWithSmallerValue() { val summary = createVotesSummary() summary.maxRenderFrameRate = 120f @@ -68,7 +68,7 @@ class RenderVoteTest { } @Test - fun `does not update maxRenderFrameRate if summary has less`() { + fun doesNotUpdateMaxPRenderFrameRateWithBiggerValue() { val summary = createVotesSummary() summary.maxRenderFrameRate = 75f @@ -78,7 +78,7 @@ class RenderVoteTest { } @Test - fun `updates minPhysicalRefreshRate if summary has less`() { + fun updatesMinPhysicalRefreshRateWithBiggerValue() { val summary = createVotesSummary() summary.minPhysicalRefreshRate = 45f @@ -88,7 +88,7 @@ class RenderVoteTest { } @Test - fun `does not update minPhysicalRefreshRate if summary has more`() { + fun doesNotUpdateMinPhysicalRefreshRateWithSmallerValue() { val summary = createVotesSummary() summary.minPhysicalRefreshRate = 75f diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/RequestedRefreshRateVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/RequestedRefreshRateVoteTest.kt index dbe9e4ae5ef5..be9c5631bbe4 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/RequestedRefreshRateVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/RequestedRefreshRateVoteTest.kt @@ -28,7 +28,7 @@ import org.junit.runner.RunWith class RequestedRefreshRateVoteTest { @Test - fun `updates requestedRefreshRates`() { + fun testUpdatesRequestedRefreshRates() { val refreshRate = 90f val vote = RequestedRefreshRateVote(refreshRate) val summary = createVotesSummary() @@ -40,7 +40,7 @@ class RequestedRefreshRateVoteTest { } @Test - fun `updates requestedRefreshRates with multiple refresh rates`() { + fun testUpdatesRequestedRefreshRates_multipleVotes() { val refreshRate1 = 90f val vote1 = RequestedRefreshRateVote(refreshRate1) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt index 4fc574a77571..d7dcca7b18f7 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SettingsObserverTest.kt @@ -103,7 +103,7 @@ class SettingsObserverTest { } @Test - fun `test low power mode`(@TestParameter testCase: LowPowerTestCase) { + fun testLowPowerMode(@TestParameter testCase: LowPowerTestCase) { whenever(mockFlags.isVsyncLowPowerVoteEnabled).thenReturn(testCase.vsyncLowPowerVoteEnabled) whenever(spyContext.contentResolver) .thenReturn(settingsProviderRule.mockContentResolver(null)) @@ -151,7 +151,7 @@ class SettingsObserverTest { } @Test - fun `test settings refresh rates`(@TestParameter testCase: SettingsRefreshRateTestCase) { + fun testSettingsRefreshRates(@TestParameter testCase: SettingsRefreshRateTestCase) { whenever(mockFlags.isPeakRefreshRatePhysicalLimitEnabled) .thenReturn(testCase.peakRefreshRatePhysicalLimitEnabled) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt index 1be2fbffbe79..319c21e53165 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt @@ -39,7 +39,7 @@ class SizeVoteTest { } @Test - fun `updates size if width and height not set and display resolution voting disabled`() { + fun updatesSize_widthAndHeightNotSet_resolutionVotingDisabled() { val summary = createVotesSummary(isDisplayResolutionRangeVotingEnabled = false) summary.width = Vote.INVALID_SIZE summary.height = Vote.INVALID_SIZE @@ -55,7 +55,7 @@ class SizeVoteTest { } @Test - fun `does not update size if width set and display resolution voting disabled`() { + fun doesNotUpdateSiz_widthSet_resolutionVotingDisabled() { val summary = createVotesSummary(isDisplayResolutionRangeVotingEnabled = false) summary.width = 150 summary.height = Vote.INVALID_SIZE @@ -71,7 +71,7 @@ class SizeVoteTest { } @Test - fun `does not update size if height set and display resolution voting disabled`() { + fun doesNotUpdateSize_heightSet_resolutionVotingDisabled() { val summary = createVotesSummary(isDisplayResolutionRangeVotingEnabled = false) summary.width = Vote.INVALID_SIZE summary.height = 250 @@ -87,7 +87,7 @@ class SizeVoteTest { } @Test - fun `updates width if summary has more and display resolution voting enabled`() { + fun updatesWidthWithSmallerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.width = 850 @@ -97,7 +97,7 @@ class SizeVoteTest { } @Test - fun `does not update width if summary has less and display resolution voting enabled`() { + fun doesNotUpdateWidthWithBiggerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.width = 750 @@ -107,7 +107,7 @@ class SizeVoteTest { } @Test - fun `updates height if summary has more and display resolution voting enabled`() { + fun updatesHeightWithSmallerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.height = 1650 @@ -117,7 +117,7 @@ class SizeVoteTest { } @Test - fun `does not update height if summary has less and display resolution voting enabled`() { + fun doesNotUpdateHeightWithBiggerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.height = 1550 @@ -127,7 +127,7 @@ class SizeVoteTest { } @Test - fun `updates minWidth if summary has less and display resolution voting enabled`() { + fun updatesMinWidthWithSmallerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.width = 150 summary.minWidth = 350 @@ -138,7 +138,7 @@ class SizeVoteTest { } @Test - fun `does not update minWidth if summary has more and display resolution voting enabled`() { + fun doesNotUpdateMinWidthWithBiggerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.width = 150 summary.minWidth = 450 @@ -149,7 +149,7 @@ class SizeVoteTest { } @Test - fun `updates minHeight if summary has less and display resolution voting enabled`() { + fun updatesMinHeightWithSmallerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.width = 150 summary.minHeight = 1150 @@ -160,7 +160,7 @@ class SizeVoteTest { } @Test - fun `does not update minHeight if summary has more and display resolution voting enabled`() { + fun doesNotUpdateMinHeightWithBiggerValue_resolutionVotingEnabled() { val summary = createVotesSummary() summary.width = 150 summary.minHeight = 1250 diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt index 6ce49b8cb31e..2a50a33d07ab 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt @@ -39,7 +39,7 @@ class SupportedModesVoteTest { } @Test - fun `adds supported mode ids if supportedModeIds in summary is null`() { + fun addsSupportedModeIds_summaryHasNull() { val summary = createVotesSummary() supportedModesVote.updateSummary(summary) @@ -48,7 +48,7 @@ class SupportedModesVoteTest { } @Test - fun `does not add supported mode ids if summary has empty list of modeIds`() { + fun doesNotAddSupportedModeIdes_summaryHasEmptyList() { val summary = createVotesSummary() summary.supportedModeIds = ArrayList() @@ -58,7 +58,7 @@ class SupportedModesVoteTest { } @Test - fun `filters out modes that does not match vote`() { + fun filtersModeIdsThatDoesNotMatchVote() { val summary = createVotesSummary() summary.supportedModeIds = ArrayList(listOf(otherMode, supportedModes[0])) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedRefreshRatesVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedRefreshRatesVoteTest.kt index d0c112be24a2..0da688511096 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedRefreshRatesVoteTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedRefreshRatesVoteTest.kt @@ -42,7 +42,7 @@ class SupportedRefreshRatesVoteTest { } @Test - fun `adds supported refresh rates if supportedModes in summary is null`() { + fun addsSupportedRefreshRates_summaryHasNull() { val summary = createVotesSummary() supportedRefreshRatesVote.updateSummary(summary) @@ -51,7 +51,7 @@ class SupportedRefreshRatesVoteTest { } @Test - fun `does not add supported refresh rates if summary has empty list of refresh rates`() { + fun doesNotAddSupportedRefreshRates_summaryHasEmptyList() { val summary = createVotesSummary() summary.supportedRefreshRates = ArrayList() @@ -61,7 +61,7 @@ class SupportedRefreshRatesVoteTest { } @Test - fun `filters out supported refresh rates that does not match vote`() { + fun filtersSupportedRefreshRatesThatDoesNotMatchVote() { val summary = createVotesSummary() summary.supportedRefreshRates = ArrayList(listOf(otherMode, refreshRates[0])) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt index 5cd3a336ec11..b2d83d744ce6 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt @@ -41,7 +41,7 @@ class SyntheticModeManagerTest { private val mockConfig = mock<DisplayDeviceConfig>() @Test - fun `test app supported modes`(@TestParameter testCase: AppSupportedModesTestCase) { + fun testAppSupportedModes(@TestParameter testCase: AppSupportedModesTestCase) { whenever(mockFlags.isSynthetic60HzModesEnabled).thenReturn(testCase.syntheticModesEnabled) whenever(mockConfig.isVrrSupportEnabled).thenReturn(testCase.vrrSupported) val syntheticModeManager = SyntheticModeManager(mockFlags) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SystemRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SystemRequestObserverTest.kt index c49205bcfe3d..9ea7ea7ef23d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/SystemRequestObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SystemRequestObserverTest.kt @@ -51,7 +51,7 @@ class SystemRequestObserverTest { private val storage = VotesStorage({}, null) @Test - fun `requestDisplayModes adds vote to storage`() { + fun testRequestDisplayModes_voteAdded() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) @@ -69,7 +69,7 @@ class SystemRequestObserverTest { } @Test - fun `requestDisplayModes overrides votes in storage`() { + fun testRequestDisplayModes_voteReplaced() { val systemRequestObserver = SystemRequestObserver(storage) systemRequestObserver.requestDisplayModes(mockToken, DISPLAY_ID, intArrayOf(1, 2, 3)) @@ -89,7 +89,7 @@ class SystemRequestObserverTest { } @Test - fun `requestDisplayModes removes vote to storage`() { + fun testRequestDisplayModes_voteRemoved() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) @@ -101,7 +101,7 @@ class SystemRequestObserverTest { } @Test - fun `requestDisplayModes calls linkToDeath to token`() { + fun testTokenLinkToDeath() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) @@ -111,7 +111,7 @@ class SystemRequestObserverTest { } @Test - fun `does not add votes to storage if binder died when requestDisplayModes called`() { + fun testBinderDied_voteRemoved() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) @@ -123,7 +123,7 @@ class SystemRequestObserverTest { } @Test - fun `removes all votes from storage when binder dies`() { + fun testBinderDied_allVotesRemoved() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) @@ -138,7 +138,7 @@ class SystemRequestObserverTest { } @Test - fun `calls unlinkToDeath on token when no votes remaining`() { + fun testTokenUnlinkToDeath_noMoreVotes() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) @@ -149,7 +149,7 @@ class SystemRequestObserverTest { } @Test - fun `does not call unlinkToDeath on token when votes for other display in storage`() { + fun testTokenUnlinkToDeathNotCalled_votesForOtherDisplayInStorage() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) @@ -161,7 +161,7 @@ class SystemRequestObserverTest { } @Test - fun `requestDisplayModes subset modes from different tokens`() { + fun testRequestDisplayModes_differentToken_voteHasModesSubset() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) systemRequestObserver.requestDisplayModes(mockToken, DISPLAY_ID, requestedModes) @@ -187,7 +187,7 @@ class SystemRequestObserverTest { } @Test - fun `recalculates vote if one binder dies`() { + fun testBinderDies_recalculatesVotes() { val systemRequestObserver = SystemRequestObserver(storage) val requestedModes = intArrayOf(1, 2, 3) systemRequestObserver.requestDisplayModes(mockToken, DISPLAY_ID, requestedModes) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt index dd5e1be8462c..239e59b69187 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt @@ -80,7 +80,7 @@ class VoteSummaryTest { } @Test - fun `filters modes for summary supportedRefreshRates`( + fun testFiltersModes_supportedRefreshRates( @TestParameter testCase: SupportedRefreshRatesTestCase ) { val summary = createSummary(testCase.supportedModesVoteEnabled) @@ -142,9 +142,7 @@ class VoteSummaryTest { } @Test - fun `filters modes for summary supportedModes`( - @TestParameter testCase: SupportedModesTestCase - ) { + fun testFiltersModes_supportedModes(@TestParameter testCase: SupportedModesTestCase) { val summary = createSummary(testCase.supportedModesVoteEnabled) summary.supportedModeIds = testCase.summarySupportedModes @@ -154,7 +152,7 @@ class VoteSummaryTest { } @Test - fun `summary invalid if has requestedRefreshRate less than minRenederRate`() { + fun testInvalidSummary_requestedRefreshRateLessThanMinRenderRate() { val summary = createSummary() summary.requestedRefreshRates = setOf(30f, 90f) summary.minRenderFrameRate = 60f @@ -166,7 +164,7 @@ class VoteSummaryTest { } @Test - fun `summary invalid if has requestedRefreshRate more than maxRenderFrameRate`() { + fun testInvalidSummary_requestedRefreshRateMoreThanMaxRenderRate() { val summary = createSummary() summary.requestedRefreshRates = setOf(60f, 240f) summary.minRenderFrameRate = 60f @@ -178,7 +176,7 @@ class VoteSummaryTest { } @Test - fun `summary valid if all requestedRefreshRates inside render rate limits`() { + fun testValidSummary_requestedRefreshRatesWithingRenderRateLimits() { val summary = createSummary() summary.requestedRefreshRates = setOf(60f, 90f) summary.minRenderFrameRate = 60f diff --git a/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java b/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java index 1c4db6ad883b..c1d7c7b4a4c2 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/WakeLockLogTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.PackageManager; import android.os.PowerManager; +import android.os.Process; import org.junit.Before; import org.junit.Test; @@ -54,6 +55,8 @@ public class WakeLockLogTest { when(mPackageManager.getPackagesForUid(101)).thenReturn(new String[]{ "some.package1" }); when(mPackageManager.getPackagesForUid(102)).thenReturn(new String[]{ "some.package2" }); + when(mPackageManager.getPackagesForUid(Process.SYSTEM_UID)) + .thenReturn(new String[]{ "some.package3" }); } @Test @@ -70,14 +73,20 @@ public class WakeLockLogTest { log.onWakeLockAcquired("TagFull", 102, PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, -1); + when(injectorSpy.currentTimeMillis()).thenReturn(1250L); + log.onWakeLockAcquired("TagSystem", 1000, + PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, -1); + assertEquals("Wake Lock Log\n" + " 01-01 00:00:01.000 - 101 (some.package1) - ACQ TagPartial " + "(partial,on-after-release)\n" + " 01-01 00:00:01.150 - 102 (some.package2) - ACQ TagFull " + "(full,acq-causes-wake)\n" + + " 01-01 00:00:01.250 - 1000 (" + WakeLockLog.SYSTEM_PACKAGE_NAME + ")" + + " - ACQ TagSystem (full,acq-causes-wake)\n" + " -\n" - + " Events: 2, Time-Resets: 0\n" - + " Buffer, Bytes used: 6\n", + + " Events: 3, Time-Resets: 0\n" + + " Buffer, Bytes used: 9\n", dumpLog(log, false)); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java index 4ec2fb9ffe6e..cdaeade41159 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/a11ychecker/AccessibilityCheckerUtilsTest.java @@ -89,14 +89,15 @@ public class AccessibilityCheckerUtilsTest { AccessibilityCheckResult.AccessibilityCheckResultType.NOT_RUN, null, 5, null); - Set<AndroidAccessibilityCheckerResult> results = - AccessibilityCheckerUtils.processResults( - mockNodeInfo, - List.of(result1, result2, result3, result4), - null, + + AndroidAccessibilityCheckerResult.Builder resultBuilder = + AccessibilityCheckerUtils.getCommonResultBuilder(mockNodeInfo, null, mMockPackageManager, new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME)); + Set<AndroidAccessibilityCheckerResult> results = + AccessibilityCheckerUtils.processResults(mockNodeInfo, + List.of(result1, result2, result3, result4), resultBuilder); assertThat(results).containsExactly( createResult("TargetNode", "", @@ -128,14 +129,14 @@ public class AccessibilityCheckerUtilsTest { TouchTargetSizeCheck.class, AccessibilityCheckResult.AccessibilityCheckResultType.ERROR, null, 2, null); - Set<AndroidAccessibilityCheckerResult> results = - AccessibilityCheckerUtils.processResults( - mockNodeInfo, - List.of(result1, result2), - null, + AndroidAccessibilityCheckerResult.Builder resultBuilder = + AccessibilityCheckerUtils.getCommonResultBuilder(mockNodeInfo, null, mMockPackageManager, new ComponentName(TEST_A11Y_SERVICE_SOURCE_PACKAGE_NAME, TEST_A11Y_SERVICE_CLASS_NAME)); + Set<AndroidAccessibilityCheckerResult> results = + AccessibilityCheckerUtils.processResults(mockNodeInfo, + List.of(result1, result2), resultBuilder); assertThat(results).isEmpty(); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 957ee06b6e27..598d3a3a9f8a 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -23,6 +23,8 @@ import static android.view.MotionEvent.ACTION_POINTER_DOWN; import static android.view.MotionEvent.ACTION_POINTER_INDEX_SHIFT; import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; +import static android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE; +import static android.view.MotionEvent.TOOL_TYPE_FINGER; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -1414,6 +1416,49 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test + public void testSynthesizedGestureEventsDoNotMoveMagnifierViewport() { + final EventCaptor eventCaptor = new EventCaptor(); + mMgh.setNext(eventCaptor); + + float centerX = + (INITIAL_MAGNIFICATION_BOUNDS.left + INITIAL_MAGNIFICATION_BOUNDS.width()) / 2.0f; + float centerY = + (INITIAL_MAGNIFICATION_BOUNDS.top + INITIAL_MAGNIFICATION_BOUNDS.height()) / 2.0f; + float scale = 5.6f; // value is unimportant but unique among tests to increase coverage. + mFullScreenMagnificationController.setScaleAndCenter( + DISPLAY_0, centerX, centerY, scale, /* animate= */ false, 1); + centerX = mFullScreenMagnificationController.getCenterX(DISPLAY_0); + centerY = mFullScreenMagnificationController.getCenterY(DISPLAY_0); + + // Second finger down on trackpad starts a synthesized two-finger swipe with source + // mouse. + MotionEvent downEvent = motionEvent(centerX, centerY, ACTION_DOWN, + TOOL_TYPE_FINGER, CLASSIFICATION_TWO_FINGER_SWIPE); + send(downEvent, InputDevice.SOURCE_MOUSE); + fastForward(20); + + // Two-finger swipe creates a synthesized move event, and shouldn't impact magnifier + // viewport. + MotionEvent moveEvent = motionEvent(centerX - 42, centerY - 42, ACTION_MOVE, + TOOL_TYPE_FINGER, CLASSIFICATION_TWO_FINGER_SWIPE); + send(moveEvent, InputDevice.SOURCE_MOUSE); + fastForward(20); + + assertThat(mFullScreenMagnificationController.getCenterX(DISPLAY_0)).isEqualTo(centerX); + assertThat(mFullScreenMagnificationController.getCenterY(DISPLAY_0)).isEqualTo(centerY); + + // The events were not consumed by magnifier. + assertThat(eventCaptor.mEvents.size()).isEqualTo(2); + assertThat(eventCaptor.mEvents.get(0).getSource()).isEqualTo(InputDevice.SOURCE_MOUSE); + assertThat(eventCaptor.mEvents.get(1).getSource()).isEqualTo(InputDevice.SOURCE_MOUSE); + + final List<Integer> expectedActions = new ArrayList(); + expectedActions.add(Integer.valueOf(ACTION_DOWN)); + expectedActions.add(Integer.valueOf(ACTION_MOVE)); + assertActionsInOrder(eventCaptor.mEvents, expectedActions); + } + + @Test @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) public void testMouseHoverMoveEventsDoNotMoveMagnifierViewport() { runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE); @@ -2130,6 +2175,30 @@ public class FullScreenMagnificationGestureHandlerTest { return MotionEvent.obtain(mLastDownTime, mClock.now(), action, x, y, 0); } + private MotionEvent motionEvent(float x, float y, int action, int toolType, + int classification) { + // Create a generic motion event to populate the parameters. + MotionEvent event = motionEvent(x, y, action); + int pointerCount = event.getPointerCount(); + MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[pointerCount]; + MotionEvent.PointerProperties[] properties = + new MotionEvent.PointerProperties[pointerCount]; + for (int i = 0; i < pointerCount; i++) { + properties[i] = new MotionEvent.PointerProperties(); + event.getPointerProperties(i, properties[i]); + properties[i].toolType = toolType; + coords[i] = new MotionEvent.PointerCoords(); + event.getPointerCoords(i, coords[i]); + } + // Apply the custom classification. + return MotionEvent.obtain(event.getDownTime(), event.getEventTime(), action, + /*pointerCount=*/1, properties, coords, + event.getMetaState(), event.getButtonState(), + event.getXPrecision(), event.getYPrecision(), event.getDeviceId(), + event.getEdgeFlags(), event.getSource(), event.getDisplayId(), event.getFlags(), + classification); + } + private MotionEvent mouseEvent(float x, float y, int action) { return fromMouse(motionEvent(x, y, action)); } diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 1db46bf17655..2a55521ab329 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -55,6 +55,7 @@ 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.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; @@ -145,7 +146,6 @@ import java.util.stream.Stream; */ @SmallTest @Presubmit - public class UserControllerTest { // Use big enough user id to avoid picking up already active user id. private static final int TEST_USER_ID = 100; @@ -593,6 +593,7 @@ public class UserControllerTest { @Test public void testScheduleStopOfBackgroundUser_switch() { mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SCHEDULE_STOP_OF_BACKGROUND_USER); + assumeFalse(UserManager.isVisibleBackgroundUsersEnabled()); mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 10, /* delayUserDataLocking= */ false, @@ -642,6 +643,7 @@ public class UserControllerTest { @Test public void testScheduleStopOfBackgroundUser_startInBackground() throws Exception { mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SCHEDULE_STOP_OF_BACKGROUND_USER); + assumeFalse(UserManager.isVisibleBackgroundUsersEnabled()); mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 10, /* delayUserDataLocking= */ false, @@ -681,6 +683,7 @@ public class UserControllerTest { @Test public void testScheduleStopOfBackgroundUser_rescheduleWhenGuest() throws Exception { mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SCHEDULE_STOP_OF_BACKGROUND_USER); + assumeFalse(UserManager.isVisibleBackgroundUsersEnabled()); mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 10, /* delayUserDataLocking= */ false, @@ -736,6 +739,7 @@ public class UserControllerTest { @Test public void testScheduleStopOfBackgroundUser_rescheduleIfAlarm() throws Exception { mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SCHEDULE_STOP_OF_BACKGROUND_USER); + assumeFalse(UserManager.isVisibleBackgroundUsersEnabled()); mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 10, /* delayUserDataLocking= */ false, diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java index 5a78d9e947b8..1a593dd9baba 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java @@ -37,7 +37,6 @@ import static org.mockito.Mockito.when; import android.app.WindowConfiguration; import android.companion.virtual.IVirtualDeviceIntentInterceptor; -import android.companion.virtual.VirtualDeviceManager; import android.content.AttributionSource; import android.content.ComponentName; import android.content.Context; @@ -94,15 +93,9 @@ public class GenericWindowPolicyControllerTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock - private VirtualDeviceManager.ActivityListener mActivityListener; - @Mock - private GenericWindowPolicyController.IntentListenerCallback mIntentListenerCallback; - @Mock - private GenericWindowPolicyController.ActivityBlockedCallback mActivityBlockedCallback; + private GenericWindowPolicyController.ActivityListener mActivityListener; @Mock private GenericWindowPolicyController.RunningAppsChangedListener mRunningAppsChangedListener; - @Mock - private GenericWindowPolicyController.SecureWindowCallback mSecureWindowCallback; @Before public void setUp() throws Exception { @@ -669,14 +662,14 @@ public class GenericWindowPolicyControllerTest { /* targetDisplayCategory */ null); // register interceptor and intercept intent - when(mIntentListenerCallback.shouldInterceptIntent(any(Intent.class))).thenReturn(true); + when(mActivityListener.shouldInterceptIntent(any(Intent.class))).thenReturn(true); assertThat(gwpc.canActivityBeLaunched(activityInfo, intent, WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /* isNewTask= */false, /* isResultExpected = */ false, /* intentSender= */ null)) .isFalse(); // unregister interceptor and launch activity - when(mIntentListenerCallback.shouldInterceptIntent(any(Intent.class))).thenReturn(false); + when(mActivityListener.shouldInterceptIntent(any(Intent.class))).thenReturn(false); assertThat(gwpc.canActivityBeLaunched(activityInfo, intent, WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /* isNewTask= */false, /* isResultExpected = */ false, /* intentSender= */ null)) @@ -696,13 +689,12 @@ public class GenericWindowPolicyControllerTest { /* targetDisplayCategory */ null); // register interceptor with different filter - when(mIntentListenerCallback.shouldInterceptIntent(any(Intent.class))).thenReturn(false); + when(mActivityListener.shouldInterceptIntent(any(Intent.class))).thenReturn(false); assertThat(gwpc.canActivityBeLaunched(activityInfo, intent, WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, /* isNewTask= */false, /* isResultExpected = */ false, /* intentSender= */ null)) .isTrue(); - verify(mIntentListenerCallback, timeout(TIMEOUT_MILLIS)) - .shouldInterceptIntent(any(Intent.class)); + verify(mActivityListener, timeout(TIMEOUT_MILLIS)).shouldInterceptIntent(any(Intent.class)); } @Test @@ -723,8 +715,8 @@ public class GenericWindowPolicyControllerTest { /* isResultExpected = */ true, /* intentSender= */ () -> intentSender)) .isFalse(); - verify(mActivityBlockedCallback, timeout(TIMEOUT_MILLIS)) - .onActivityBlocked(DISPLAY_ID, activityInfo, /* intentSender= */ null); + verify(mActivityListener, timeout(TIMEOUT_MILLIS)) + .onActivityLaunchBlocked(DISPLAY_ID, activityInfo, /* intentSender= */ null); } @Test @@ -761,10 +753,10 @@ public class GenericWindowPolicyControllerTest { assertThat(gwpc.keepActivityOnWindowFlagsChanged(activityInfo, 0, 0)).isTrue(); - verify(mSecureWindowCallback, after(TIMEOUT_MILLIS).never()) - .onSecureWindowShown(DISPLAY_ID, activityInfo.applicationInfo.uid); - verify(mActivityBlockedCallback, never()) - .onActivityBlocked(eq(DISPLAY_ID), eq(activityInfo), any()); + verify(mActivityListener, after(TIMEOUT_MILLIS).never()) + .onSecureWindowShown(eq(DISPLAY_ID), eq(activityInfo)); + verify(mActivityListener, never()) + .onActivityLaunchBlocked(eq(DISPLAY_ID), eq(activityInfo), any()); } @Test @@ -780,10 +772,10 @@ public class GenericWindowPolicyControllerTest { assertThat(gwpc.keepActivityOnWindowFlagsChanged(activityInfo, FLAG_SECURE, 0)).isTrue(); - verify(mSecureWindowCallback, timeout(TIMEOUT_MILLIS)).onSecureWindowShown(DISPLAY_ID, - activityInfo.applicationInfo.uid); - verify(mActivityBlockedCallback, after(TIMEOUT_MILLIS).never()) - .onActivityBlocked(eq(DISPLAY_ID), eq(activityInfo), any()); + verify(mActivityListener, timeout(TIMEOUT_MILLIS)) + .onSecureWindowShown(eq(DISPLAY_ID), eq(activityInfo)); + verify(mActivityListener, after(TIMEOUT_MILLIS).never()) + .onActivityLaunchBlocked(eq(DISPLAY_ID), eq(activityInfo), any()); } @Test @@ -800,10 +792,10 @@ public class GenericWindowPolicyControllerTest { assertThat(gwpc.keepActivityOnWindowFlagsChanged(activityInfo, 0, SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS)).isTrue(); - verify(mSecureWindowCallback, after(TIMEOUT_MILLIS).never()) - .onSecureWindowShown(DISPLAY_ID, activityInfo.applicationInfo.uid); - verify(mActivityBlockedCallback, never()) - .onActivityBlocked(eq(DISPLAY_ID), eq(activityInfo), any()); + verify(mActivityListener, after(TIMEOUT_MILLIS).never()) + .onSecureWindowShown(eq(DISPLAY_ID), eq(activityInfo)); + verify(mActivityListener, never()) + .onActivityLaunchBlocked(eq(DISPLAY_ID), eq(activityInfo), any()); } @Test @@ -835,9 +827,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ new ArraySet<>(), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ mSecureWindowCallback, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); @@ -855,9 +844,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ new ArraySet<>(), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ mSecureWindowCallback, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); @@ -876,9 +862,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ new ArraySet<>(), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ null, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ homeComponent); @@ -897,9 +880,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ new ArraySet<>(), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ null, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); @@ -918,9 +898,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ new ArraySet<>(), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ null, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); @@ -939,9 +916,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ new ArraySet<>(), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ null, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ Collections.singleton(displayCategory), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); @@ -960,9 +934,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ Collections.singleton(blockedComponent), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ null, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); @@ -981,9 +952,6 @@ public class GenericWindowPolicyControllerTest { /* crossTaskNavigationAllowedByDefault= */ false, /* crossTaskNavigationExemptions= */ Collections.singleton(allowedComponent), /* activityListener= */ mActivityListener, - /* activityBlockedCallback= */ mActivityBlockedCallback, - /* secureWindowCallback= */ null, - /* intentListenerCallback= */ mIntentListenerCallback, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); @@ -1029,9 +997,9 @@ public class GenericWindowPolicyControllerTest { assertThat(gwpc.canActivityBeLaunched(activityInfo, null, windowingMode, fromDisplay, isNewTask, /* isResultExpected= */ false, () -> intentSender)).isTrue(); - verify(mActivityBlockedCallback, after(TIMEOUT_MILLIS).never()) - .onActivityBlocked(fromDisplay, activityInfo, intentSender); - verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class)); + verify(mActivityListener, after(TIMEOUT_MILLIS).never()) + .onActivityLaunchBlocked(fromDisplay, activityInfo, intentSender); + verify(mActivityListener, never()).shouldInterceptIntent(any(Intent.class)); } private void assertActivityIsBlocked(GenericWindowPolicyController gwpc, @@ -1046,9 +1014,9 @@ public class GenericWindowPolicyControllerTest { assertThat(gwpc.canActivityBeLaunched(activityInfo, null, windowingMode, fromDisplay, isNewTask, /* isResultExpected= */ false, () -> intentSender)).isFalse(); - verify(mActivityBlockedCallback, timeout(TIMEOUT_MILLIS)) - .onActivityBlocked(fromDisplay, activityInfo, intentSender); - verify(mIntentListenerCallback, after(TIMEOUT_MILLIS).never()) + verify(mActivityListener, timeout(TIMEOUT_MILLIS)) + .onActivityLaunchBlocked(fromDisplay, activityInfo, intentSender); + verify(mActivityListener, after(TIMEOUT_MILLIS).never()) .shouldInterceptIntent(any(Intent.class)); } @@ -1060,8 +1028,8 @@ public class GenericWindowPolicyControllerTest { /* isResultExpected= */ false, () -> intentSender)) .isFalse(); - verify(mActivityBlockedCallback, after(TIMEOUT_MILLIS).never()) - .onActivityBlocked(eq(fromDisplay), eq(activityInfo), any()); - verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class)); + verify(mActivityListener, after(TIMEOUT_MILLIS).never()) + .onActivityLaunchBlocked(eq(fromDisplay), eq(activityInfo), any()); + verify(mActivityListener, never()).shouldInterceptIntent(any(Intent.class)); } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java index 405929a5023b..51c2ad1d1134 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java @@ -87,9 +87,6 @@ public class VirtualAudioControllerTest { /* crossTaskNavigationAllowedByDefault= */ true, /* crossTaskNavigationExemptions= */ new ArraySet<>(), /* activityListener= */ null, - /* activityBlockedCallback= */ null, - /* secureWindowCallback= */ null, - /* intentListenerCallback= */ null, /* displayCategories= */ new ArraySet<>(), /* showTasksInHostDeviceRecents= */ true, /* customHomeComponent= */ null); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java index b2fd8aa7d405..161b18c80e77 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest7.java @@ -165,7 +165,7 @@ public class ShortcutManagerTest7 extends BaseShortcutManagerTest { assertTrue(resultContains( callShellCommand("reset-throttling", "--user", "10"), - "User 10 is not running or locked")); + "User (with userId=10) is not running or locked")); mRunningUsers.put(USER_10, true); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index e70ed5f256bf..f8ff1f45e89c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -788,6 +788,19 @@ public class ZenModeConfigTest extends UiServiceTestCase { } @Test + public void testRuleXml_invalidInterruptionFilter_readsDefault() throws Exception { + ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); + rule.zenMode = 1979; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeRuleXml(rule, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig.ZenRule fromXml = readRuleXml(bais); + + assertThat(fromXml.zenMode).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + } + + @Test public void testZenPolicyXml_allUnset() throws Exception { ZenPolicy policy = new ZenPolicy.Builder().build(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index c1e3f47679ca..baa633f16f67 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -18,6 +18,8 @@ package com.android.server.notification; import static android.app.AutomaticZenRule.TYPE_BEDTIME; import static android.app.AutomaticZenRule.TYPE_IMMERSIVE; +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; +import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.Flags.FLAG_MODES_API; import static android.app.Flags.FLAG_MODES_UI; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; @@ -6875,6 +6877,52 @@ public class ZenModeHelperTest extends UiServiceTestCase { "Didn't find rule with id %s", ruleId); } + @Test + @DisableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void testDefaultConfig_preModesApi_rulesAreBare() { + // Create a new user, which should get a copy of the default policy. + mZenModeHelper.onUserSwitched(101); + + ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get( + ZenModeConfig.EVENTS_DEFAULT_RULE_ID); + + assertThat(eventsRule).isNotNull(); + assertThat(eventsRule.zenPolicy).isNull(); + assertThat(eventsRule.type).isEqualTo(TYPE_UNKNOWN); + assertThat(eventsRule.triggerDescription).isNull(); + } + + @Test + @EnableFlags(FLAG_MODES_API) + @DisableFlags(FLAG_MODES_UI) + public void testDefaultConfig_modesApi_rulesHaveFullPolicy() { + // Create a new user, which should get a copy of the default policy. + mZenModeHelper.onUserSwitched(201); + + ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get( + ZenModeConfig.EVENTS_DEFAULT_RULE_ID); + + assertThat(eventsRule).isNotNull(); + assertThat(eventsRule.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy()); + assertThat(eventsRule.type).isEqualTo(TYPE_UNKNOWN); + assertThat(eventsRule.triggerDescription).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void testDefaultConfig_modesUi_rulesHaveFullPolicy() { + // Create a new user, which should get a copy of the default policy. + mZenModeHelper.onUserSwitched(301); + + ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get( + ZenModeConfig.EVENTS_DEFAULT_RULE_ID); + + assertThat(eventsRule).isNotNull(); + assertThat(eventsRule.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy()); + assertThat(eventsRule.type).isEqualTo(TYPE_SCHEDULE_CALENDAR); + assertThat(eventsRule.triggerDescription).isNotEmpty(); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java index d147325921bf..0575d98b65ec 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutManagerTests.java @@ -41,6 +41,8 @@ import android.content.res.XmlResourceParser; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; import android.view.KeyboardShortcutInfo; @@ -50,6 +52,8 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import java.util.Collections; @@ -62,7 +66,13 @@ import java.util.Collections; */ @SmallTest +@EnableFlags(com.android.hardware.input.Flags.FLAG_MODIFIER_SHORTCUT_MANAGER_REFACTOR) public class ModifierShortcutManagerTests { + + @ClassRule public static final SetFlagsRule.ClassRule SET_FLAGS_CLASS_RULE = + new SetFlagsRule.ClassRule(); + @Rule public final SetFlagsRule mSetFlagsRule = SET_FLAGS_CLASS_RULE.createSetFlagsRule(); + private ModifierShortcutManager mModifierShortcutManager; private Handler mHandler; private Context mContext; diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java index 71f90a2560fd..43171f847818 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java @@ -44,26 +44,21 @@ import android.app.role.RoleManager; import android.content.ComponentName; import android.content.Intent; import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.SparseArray; import androidx.test.filters.SmallTest; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; @Presubmit @SmallTest +@EnableFlags(com.android.hardware.input.Flags.FLAG_MODIFIER_SHORTCUT_MANAGER_REFACTOR) public class ModifierShortcutTests extends ShortcutKeyTestBase { - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - private static final SparseArray<String> INTENT_SHORTCUTS = new SparseArray<>(); private static final SparseArray<String> ROLE_SHORTCUTS = new SparseArray<>(); static { @@ -258,7 +253,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * META+CTRL+BACKSPACE for taking a bugreport when the flag is enabled. */ @Test - @RequiresFlagsEnabled(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT) + @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT) public void testTakeBugReport_flagEnabled() throws RemoteException { sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0); mPhoneWindowManager.assertTakeBugreport(true); @@ -268,7 +263,7 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * META+CTRL+BACKSPACE for taking a bugreport does nothing when the flag is disabledd. */ @Test - @RequiresFlagsDisabled(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT) + @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT) public void testTakeBugReport_flagDisabled() throws RemoteException { sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0); mPhoneWindowManager.assertTakeBugreport(false); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java index 6e488188eb87..3910904337b2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerExemptionTests.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_ALLOWLISTED_COMPONENT; @@ -25,9 +26,11 @@ import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_ import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_SAW_PERMISSION; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW; +import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK; import static com.google.common.truth.Truth.assertWithMessage; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -43,6 +46,9 @@ import android.content.Intent; import android.content.pm.PackageManagerInternal; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DeviceConfig; import android.util.Pair; @@ -52,6 +58,7 @@ import com.android.compatibility.common.util.DeviceConfigStateHelper; import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.am.PendingIntentRecord; import com.android.server.wm.BackgroundActivityStartController.BalVerdict; +import com.android.window.flags.Flags; import org.junit.After; import org.junit.Before; @@ -95,7 +102,9 @@ public class BackgroundActivityStartControllerExemptionTests { @Rule public final ExtendedMockitoRule extendedMockitoRule = new ExtendedMockitoRule.Builder(this).setStrictness(Strictness.LENIENT).build(); - + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); TestableBackgroundActivityStartController mController; @Mock ActivityMetricsLogger mActivityMetricsLogger; @@ -186,7 +195,7 @@ public class BackgroundActivityStartControllerExemptionTests { when(mAppOpsManager.checkOpNoThrow( eq(AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION), anyInt(), anyString())).thenReturn(AppOpsManager.MODE_DEFAULT); - when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn( + when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt(), any())).thenReturn( BalVerdict.BLOCK); } @@ -227,7 +236,7 @@ public class BackgroundActivityStartControllerExemptionTests { // call BalVerdict callerVerdict = mController.checkBackgroundActivityStartAllowedByCaller( balState); - BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender( + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( balState); balState.setResultForCaller(callerVerdict); @@ -295,7 +304,77 @@ public class BackgroundActivityStartControllerExemptionTests { checkedOptions); // call - BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender( + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( + balState); + balState.setResultForRealCaller(realCallerVerdict); + + // assertions + assertWithMessage(balState.toString()).that(realCallerVerdict.getCode()).isEqualTo( + BAL_ALLOW_VISIBLE_WINDOW); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BAL_ADDITIONAL_START_MODES) + public void testCaller_appHasVisibleWindowWithIfVisibleOptIn() { + int callingUid = REGULAR_UID_1; + int callingPid = REGULAR_PID_1; + final String callingPackage = REGULAR_PACKAGE_1; + int realCallingUid = REGULAR_UID_2; + int realCallingPid = REGULAR_PID_2; + + // setup state + when(mService.hasActiveVisibleWindow(eq(callingUid))).thenReturn(true); + when(mService.getBalAppSwitchesState()).thenReturn(APP_SWITCH_ALLOW); + + // prepare call + PendingIntentRecord originatingPendingIntent = mPendingIntentRecord; + BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE; + Intent intent = TEST_INTENT; + ActivityOptions checkedOptions = mCheckedOptions + .setPendingIntentCreatorBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE); + BackgroundActivityStartController.BalState balState = mController.new BalState(callingUid, + callingPid, callingPackage, realCallingUid, realCallingPid, mCallerApp, + originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent, + checkedOptions); + + // call + BalVerdict callerVerdict = mController.checkBackgroundActivityStartAllowedByCaller( + balState); + balState.setResultForCaller(callerVerdict); + + // assertions + assertWithMessage(balState.toString()).that(callerVerdict.getCode()).isEqualTo( + BAL_ALLOW_VISIBLE_WINDOW); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_BAL_ADDITIONAL_START_MODES) + public void testRealCaller_appHasVisibleWindowWithIfVisibleOptIn() { + int callingUid = REGULAR_UID_1; + int callingPid = REGULAR_PID_1; + final String callingPackage = REGULAR_PACKAGE_1; + int realCallingUid = REGULAR_UID_2; + int realCallingPid = REGULAR_PID_2; + + // setup state + when(mService.hasActiveVisibleWindow(eq(realCallingUid))).thenReturn(true); + when(mService.getBalAppSwitchesState()).thenReturn(APP_SWITCH_ALLOW); + + // prepare call + PendingIntentRecord originatingPendingIntent = mPendingIntentRecord; + BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE; + Intent intent = TEST_INTENT; + ActivityOptions checkedOptions = mCheckedOptions + .setPendingIntentCreatorBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE); + BackgroundActivityStartController.BalState balState = mController.new BalState(callingUid, + callingPid, callingPackage, realCallingUid, realCallingPid, mCallerApp, + originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent, + checkedOptions); + + // call + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( balState); balState.setResultForRealCaller(realCallerVerdict); @@ -320,7 +399,7 @@ public class BackgroundActivityStartControllerExemptionTests { int realCallingPid = REGULAR_PID_2; // setup state - when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn( + when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt(), any())).thenReturn( new BalVerdict(BAL_ALLOW_FOREGROUND, false, "allowed")); when(mService.getBalAppSwitchesState()).thenReturn(APP_SWITCH_ALLOW); @@ -357,7 +436,7 @@ public class BackgroundActivityStartControllerExemptionTests { mService.getProcessController(eq(realCallingPid), eq(realCallingUid))).thenReturn( mCallerApp); when(mService.getBalAppSwitchesState()).thenReturn(APP_SWITCH_ALLOW); - when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn( + when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt(), any())).thenReturn( new BalVerdict(BAL_ALLOW_FOREGROUND, false, "allowed")); // prepare call @@ -371,7 +450,7 @@ public class BackgroundActivityStartControllerExemptionTests { checkedOptions); // call - BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender( + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( balState); balState.setResultForRealCaller(realCallerVerdict); @@ -404,9 +483,9 @@ public class BackgroundActivityStartControllerExemptionTests { mService.getProcessController(eq(realCallingPid), eq(realCallingUid))).thenReturn( mCallerApp); when(mService.getBalAppSwitchesState()).thenReturn(APP_SWITCH_ALLOW); - when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn( + when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt(), any())).thenReturn( BalVerdict.BLOCK); - when(otherProcess.areBackgroundActivityStartsAllowed(anyInt())).thenReturn( + when(otherProcess.areBackgroundActivityStartsAllowed(anyInt(), any())).thenReturn( new BalVerdict(BAL_ALLOW_FOREGROUND, false, "allowed")); // prepare call @@ -420,7 +499,7 @@ public class BackgroundActivityStartControllerExemptionTests { checkedOptions); // call - BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender( + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( balState); balState.setResultForRealCaller(realCallerVerdict); @@ -456,7 +535,7 @@ public class BackgroundActivityStartControllerExemptionTests { checkedOptions); // call - BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender( + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( balState); balState.setResultForRealCaller(realCallerVerdict); @@ -466,6 +545,45 @@ public class BackgroundActivityStartControllerExemptionTests { } @Test + @RequiresFlagsEnabled(Flags.FLAG_BAL_ADDITIONAL_START_MODES) + public void testRealCaller_isCompanionAppWithOptInIfVisible() { + // The app has a service that is bound by a different, visible app. The app bound to the + // service must remain visible for the app in the background to start activities + // successfully. + int callingUid = REGULAR_UID_1; + int callingPid = REGULAR_PID_1; + final String callingPackage = REGULAR_PACKAGE_1; + int realCallingUid = REGULAR_UID_2; + int realCallingPid = REGULAR_PID_2; + + // setup state + final int realCallingUserId = UserHandle.getUserId(realCallingUid); + when(mService.isAssociatedCompanionApp(eq(realCallingUserId), + eq(realCallingUid))).thenReturn(true); + + // prepare call + PendingIntentRecord originatingPendingIntent = mPendingIntentRecord; + BackgroundStartPrivileges forcedBalByPiSender = BackgroundStartPrivileges.NONE; + Intent intent = TEST_INTENT; + ActivityOptions checkedOptions = mCheckedOptions + .setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE); + BackgroundActivityStartController.BalState balState = mController.new BalState(callingUid, + callingPid, callingPackage, realCallingUid, realCallingPid, null, + originatingPendingIntent, forcedBalByPiSender, mResultRecord, intent, + checkedOptions); + + // call + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( + balState); + balState.setResultForRealCaller(realCallerVerdict); + + // assertions + assertWithMessage(balState.toString()).that(realCallerVerdict.getCode()).isEqualTo( + BAL_BLOCK); + } + + @Test public void testCaller_balPermission() { int callingUid = REGULAR_UID_1; int callingPid = REGULAR_PID_1; @@ -523,7 +641,7 @@ public class BackgroundActivityStartControllerExemptionTests { checkedOptions); // call - BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedBySender( + BalVerdict realCallerVerdict = mController.checkBackgroundActivityStartAllowedByRealCaller( balState); balState.setResultForRealCaller(realCallerVerdict); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index e364264fc74f..6ec789599482 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -24,6 +24,7 @@ import static com.android.window.flags.Flags.balImprovedMetrics; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -43,6 +44,7 @@ import androidx.test.filters.SmallTest; import com.android.compatibility.common.util.DeviceConfigStateHelper; import com.android.server.am.PendingIntentRecord; import com.android.server.wm.BackgroundActivityStartController.BalVerdict; +import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration; import org.junit.After; import org.junit.Before; @@ -167,9 +169,9 @@ public class BackgroundActivityStartControllerTests { } @Override - BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) { + BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) { return mRealCallerVerdict.orElseGet( - () -> super.checkBackgroundActivityStartAllowedBySender(state)); + () -> super.checkBackgroundActivityStartAllowedByRealCaller(state)); } public void setRealCallerVerdict(BalVerdict verdict) { @@ -177,11 +179,12 @@ public class BackgroundActivityStartControllerTests { } @Override - BalVerdict checkProcessAllowsBal(WindowProcessController app, BalState state) { + BalVerdict checkProcessAllowsBal(WindowProcessController app, BalState state, + BalCheckConfiguration checkConfiguration) { if (mProcessVerdicts.containsKey(app)) { return mProcessVerdicts.get(app); } - return super.checkProcessAllowsBal(app, state); + return super.checkProcessAllowsBal(app, state, checkConfiguration); } } @@ -209,7 +212,7 @@ public class BackgroundActivityStartControllerTests { Mockito.when(mAppOpsManager.checkOpNoThrow( eq(AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION), anyInt(), anyString())).thenReturn(AppOpsManager.MODE_DEFAULT); - Mockito.when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt())).thenReturn( + Mockito.when(mCallerApp.areBackgroundActivityStartsAllowed(anyInt(), any())).thenReturn( BalVerdict.BLOCK); } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java index c9c7e92d71cd..27e147d98b1f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundLaunchProcessControllerTests.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_DISALLOW; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_BOUND_BY_FOREGROUND; @@ -95,7 +96,12 @@ public class BackgroundLaunchProcessControllerTests { int mUid = 234; String mPackageName = "package.name"; int mAppSwitchState = APP_SWITCH_DISALLOW; - boolean mIsCheckingForFgsStart = false; + BackgroundLaunchProcessController.BalCheckConfiguration mBalCheckConfiguration = + new BackgroundLaunchProcessController.BalCheckConfiguration( + /* isCheckingForFgsStarts */ false, + /* checkVisibility */ true, + /* checkOtherExemptions */ true, + ACTIVITY_BG_START_GRACE_PERIOD_MS); boolean mHasActivityInVisibleTask = false; boolean mHasBackgroundActivityStartPrivileges = false; long mLastStopAppSwitchesTime = 0L; @@ -106,7 +112,7 @@ public class BackgroundLaunchProcessControllerTests { public void testNothingAllows() { BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -118,7 +124,7 @@ public class BackgroundLaunchProcessControllerTests { mHasBackgroundActivityStartPrivileges = true; BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -136,7 +142,7 @@ public class BackgroundLaunchProcessControllerTests { BackgroundStartPrivileges.ALLOW_BAL); BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -154,7 +160,7 @@ public class BackgroundLaunchProcessControllerTests { BackgroundStartPrivileges.ALLOW_BAL); BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -170,7 +176,7 @@ public class BackgroundLaunchProcessControllerTests { BackgroundStartPrivileges.ALLOW_BAL); BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -186,7 +192,7 @@ public class BackgroundLaunchProcessControllerTests { BackgroundStartPrivileges.ALLOW_BAL); BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -201,7 +207,7 @@ public class BackgroundLaunchProcessControllerTests { mHasActiveVisibleWindow.add(999); BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -216,7 +222,7 @@ public class BackgroundLaunchProcessControllerTests { mHasActiveVisibleWindow.add(999); BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -229,7 +235,7 @@ public class BackgroundLaunchProcessControllerTests { mHasActivityInVisibleTask = true; BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); @@ -245,7 +251,7 @@ public class BackgroundLaunchProcessControllerTests { mLastActivityFinishTime = now - 100; BalVerdict balVerdict = mController.areBackgroundActivityStartsAllowed( mPid, mUid, mPackageName, - mAppSwitchState, mIsCheckingForFgsStart, + mAppSwitchState, mBalCheckConfiguration, mHasActivityInVisibleTask, mHasBackgroundActivityStartPrivileges, mLastStopAppSwitchesTime, mLastActivityLaunchTime, mLastActivityFinishTime); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java index 193390833789..14276ae21899 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java @@ -16,7 +16,6 @@ package com.android.server.wm; -import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; @@ -278,7 +277,7 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ true); mWmInternal.waitForAllWindowsDrawn(mScreenUnblocker, - /* timeout= */ Integer.MAX_VALUE, DEFAULT_DISPLAY); + /* timeout= */ Integer.MAX_VALUE, INVALID_DISPLAY); mWmInternal.waitForAllWindowsDrawn(mSecondaryScreenUnblocker, /* timeout= */ Integer.MAX_VALUE, mSecondaryDisplayContent.getDisplayId()); @@ -317,50 +316,6 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase { verify(mScreenUnblocker).sendToTarget(); } - @Test - public void testWaitForAllWindowsDrawnForInvalidDisplay_usesTransitionToUnblock() { - mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH); - - final WindowState defaultDisplayWindow = createWindow(/* parent= */ null, - TYPE_BASE_APPLICATION, mDisplayContent, "DefaultDisplayWindow"); - makeWindowVisibleAndNotDrawn(defaultDisplayWindow); - - mDisplayContent.mDisplayUpdater.onDisplaySwitching(/* switching= */ true); - - mWmInternal.waitForAllWindowsDrawn(mScreenUnblocker, - /* timeout= */ Integer.MAX_VALUE, INVALID_DISPLAY); - - // Perform display update - mUniqueId = "new_default_display_unique_id"; - mDisplayContent.requestDisplayUpdate(mock(Runnable.class)); - - when(mDisplayContent.mTransitionController.inTransition()).thenReturn(true); - - // Notify that transition started collecting - captureStartTransitionCollection().getAllValues().forEach((callback) -> - callback.onCollectStarted(/* deferred= */ true)); - - // Verify that screen is not unblocked yet - verify(mScreenUnblocker, never()).sendToTarget(); - - // Make all display windows drawn - defaultDisplayWindow.mWinAnimator.mDrawState = HAS_DRAWN; - mWm.mRoot.performSurfacePlacement(); - - // Verify that default display is still not unblocked yet - // (so it doesn't use old windows drawn path) - verify(mScreenUnblocker, never()).sendToTarget(); - - // Mark start transaction as presented - when(mDisplayContent.mTransitionController.inTransition()).thenReturn(false); - captureRequestedTransition().getAllValues().forEach( - this::makeTransitionTransactionCompleted); - - // Verify that the default screen unblocker is sent only after start transaction - // of the Shell transition is presented - verify(mScreenUnblocker).sendToTarget(); - } - private void prepareSecondaryDisplay() { mSecondaryDisplayContent = createNewDisplay(); when(mSecondaryScreenUnblocker.getTarget()).thenReturn(mWm.mH); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index f2ea1c972b90..eca4d21a974e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1144,6 +1144,7 @@ public class DisplayContentTests extends WindowTestsBase { @Test public void testOrientationBehind() { + assertNull(mDisplayContent.getLastOrientationSource()); final ActivityRecord prev = new ActivityBuilder(mAtm).setCreateTask(true) .setScreenOrientation(getRotatedOrientation(mDisplayContent)).build(); prev.setVisibleRequested(false); diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt index 379b45cdf08e..c3e1a1fa2e18 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -177,6 +178,13 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa @Ignore("Not applicable to this CUJ.") override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} + @FlakyTest(bugId = 342596801) + override fun entireScreenCovered() = super.entireScreenCovered() + + @FlakyTest(bugId = 342596801) + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { /** {@inheritDoc} */ private var startDisplayBounds = Rect() diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt index adeba72c9c96..6e6a3275191f 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt @@ -196,7 +196,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : super.appLayerBecomesVisible() } - @Presubmit + @FlakyTest(bugId = 338296297) @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 753cb1ff5dd3..3f6a0bf49eb4 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -48,6 +48,13 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : RIGHT_BOTTOM } + enum class Edges { + LEFT, + RIGHT, + TOP, + BOTTOM + } + /** Wait for an app moved to desktop to finish its transition. */ private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) { wmHelper @@ -124,7 +131,8 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val displayRect = getDisplayRect(wmHelper) val insets = getWindowInsets( - context, WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars()) + context, WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars() + ) displayRect.inset(insets) val expectedWidth = displayRect.width() / 2 @@ -187,6 +195,40 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : dragWindow(startX, startY, endX, endY, wmHelper, device) } + /** Resize a desktop app from its edges. */ + fun edgeResize( + wmHelper: WindowManagerStateHelper, + motionEvent: MotionEventHelper, + edge: Edges + ) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val (startX, startY) = getStartCoordinatesForEdgeResize(windowRect, edge) + val verticalChange = when (edge) { + Edges.LEFT -> 0 + Edges.RIGHT -> 0 + Edges.TOP -> -100 + Edges.BOTTOM -> 100 + } + val horizontalChange = when (edge) { + Edges.LEFT -> -100 + Edges.RIGHT -> 100 + Edges.TOP -> 0 + Edges.BOTTOM -> 0 + } + + // The position we want to drag to + val endY = startY + verticalChange + val endX = startX + horizontalChange + + motionEvent.actionDown(startX, startY) + motionEvent.actionMove(startX, startY, endX, endY, /* steps= */100) + motionEvent.actionUp(endX, endY) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() + } + /** Drag a window from a source coordinate to a destination coordinate. */ fun dragWindow( startX: Int, startY: Int, @@ -237,6 +279,18 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : } } + private fun getStartCoordinatesForEdgeResize( + windowRect: Rect, + edge: Edges + ): Pair<Int, Int> { + return when (edge) { + Edges.LEFT -> Pair(windowRect.left, windowRect.bottom / 2) + Edges.RIGHT -> Pair(windowRect.right, windowRect.bottom / 2) + Edges.TOP -> Pair(windowRect.right / 2, windowRect.top) + Edges.BOTTOM -> Pair(windowRect.right / 2, windowRect.bottom) + } + } + /** Exit desktop mode by dragging the app handle to the top drag zone. */ fun exitDesktopWithDragToTopDragZone( wmHelper: WindowManagerStateHelper, diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt new file mode 100644 index 000000000000..083539890906 --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 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.wm.flicker.helpers + +import android.app.Instrumentation +import android.os.SystemClock +import android.view.ContentInfo.Source +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_STYLUS +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_MOVE +import android.view.MotionEvent.ACTION_UP +import android.view.MotionEvent.TOOL_TYPE_FINGER +import android.view.MotionEvent.TOOL_TYPE_MOUSE +import android.view.MotionEvent.TOOL_TYPE_STYLUS +import android.view.MotionEvent.ToolType + +/** + * Helper class for injecting a custom motion event and performing some actions. This is used for + * instrumenting input injections like stylus, mouse and touchpad. + */ +class MotionEventHelper( + private val instr: Instrumentation, + private val inputMethod: InputMethod +) { + enum class InputMethod(@ToolType val toolType: Int, @Source val source: Int) { + STYLUS(TOOL_TYPE_STYLUS, SOURCE_STYLUS), + MOUSE(TOOL_TYPE_MOUSE, SOURCE_MOUSE), + TOUCHPAD(TOOL_TYPE_FINGER, SOURCE_MOUSE) + } + + fun actionDown(x: Int, y: Int) { + injectMotionEvent(ACTION_DOWN, x, y) + } + + fun actionUp(x: Int, y: Int) { + injectMotionEvent(ACTION_UP, x, y) + } + + fun actionMove(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int) { + val incrementX = (endX - startX).toFloat() / (steps - 1) + val incrementY = (endY - startY).toFloat() / (steps - 1) + + for (i in 0..steps) { + val time = SystemClock.uptimeMillis() + val x = startX + incrementX * i + val y = startY + incrementY * i + + val moveEvent = getMotionEvent(time, time, ACTION_MOVE, x, y) + injectMotionEvent(moveEvent) + } + } + + private fun injectMotionEvent(action: Int, x: Int, y: Int): MotionEvent { + val eventTime = SystemClock.uptimeMillis() + val event = getMotionEvent(eventTime, eventTime, action, x.toFloat(), y.toFloat()) + injectMotionEvent(event) + return event + } + + private fun injectMotionEvent(event: MotionEvent) { + instr.uiAutomation.injectInputEvent(event, true, false) + } + + private fun getMotionEvent( + downTime: Long, + eventTime: Long, + action: Int, + x: Float, + y: Float, + ): MotionEvent { + val properties = MotionEvent.PointerProperties.createArray(1) + properties[0].toolType = inputMethod.toolType + properties[0].id = 1 + + val coords = MotionEvent.PointerCoords.createArray(1) + coords[0].x = x + coords[0].y = y + coords[0].pressure = 1f + + val event = + MotionEvent.obtain( + downTime, + eventTime, + action, + /* pointerCount= */ 1, + properties, + coords, + /* metaState= */ 0, + /* buttonState= */ 0, + /* xPrecision = */ 1f, + /* yPrecision = */ 1f, + /* deviceId = */ 0, + /* edgeFlags = */ 0, + inputMethod.source, + /* flags = */ 0 + ) + event.displayId = 0 + return event + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index 43fd57bf39aa..931e4f88aa8d 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -269,9 +269,23 @@ open class PipAppHelper(instrumentation: Instrumentation) : /** Expand the PIP window back to full screen via intent and wait until the app is visible */ fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) = launchViaIntent(wmHelper) - fun changeAspectRatio() { + fun changeAspectRatio(wmHelper: WindowManagerStateHelper) { val intent = Intent("com.android.wm.shell.flicker.testapp.ASPECT_RATIO") context.sendBroadcast(intent) + // Wait on WMHelper on size change upon aspect ratio change + val windowRect = getWindowRect(wmHelper) + wmHelper + .StateSyncBuilder() + .add("pipAspectRatioChanged") { + val pipAppWindow = + it.wmState.visibleWindows.firstOrNull { window -> + this.windowMatchesAnyOf(window) + } + ?: return@add false + val pipRegion = pipAppWindow.frameRegion + return@add pipRegion != Region(windowRect) + } + .waitForAndVerify() } fun clickEnterPipButton(wmHelper: WindowManagerStateHelper) { diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp index 06c2651b604d..65398a22d968 100644 --- a/tests/Input/Android.bp +++ b/tests/Input/Android.bp @@ -40,7 +40,7 @@ android_test { "frameworks-base-testutils", "hamcrest-library", "kotlin-test", - "mockito-target-minus-junit4", + "mockito-target-extended-minus-junit4", "platform-test-annotations", "platform-screenshot-diff-core", "services.core.unboosted", |