diff options
277 files changed, 4950 insertions, 2424 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/core/api/current.txt b/core/api/current.txt index ddfd364cc55d..4e6dacff290e 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 public static final String PASSWORD_COMPLEXITY_POLICY = "passwordComplexity"; + field @FlaggedApi("android.app.admin.flags.policy_engine_migration_v2_enabled") 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 public static final String USB_DATA_SIGNALING_POLICY = "usbDataSignaling"; + 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 USER_CONTROL_DISABLED_PACKAGES_POLICY = "userControlDisabledPackages"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 3637ca763e0a..e7ed8fb9f7e9 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"; @@ -4631,6 +4632,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 a1aa679f01a9..121a9ae2ac70 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 @RequiresPermission("android.permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT") public void forceSetMaxPolicyStorageLimit(int); + 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 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 @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int getHeadlessDeviceOwnerMode(); + 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 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 @RequiresPermission("android.permission.MANAGE_DEVICE_POLICY_STORAGE_LIMIT") public int getPolicySizeForAdmin(@NonNull android.app.admin.EnforcingAdmin); + 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 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 public EnforcingAdmin(@NonNull String, @NonNull android.app.admin.Authority, @NonNull android.os.UserHandle, @Nullable android.content.ComponentName); + 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); } 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/admin/AccountTypePolicyKey.java b/core/java/android/app/admin/AccountTypePolicyKey.java index 515c1c66b2a3..02e492bb06aa 100644 --- a/core/java/android/app/admin/AccountTypePolicyKey.java +++ b/core/java/android/app/admin/AccountTypePolicyKey.java @@ -24,6 +24,7 @@ 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; @@ -53,7 +54,9 @@ public final class AccountTypePolicyKey extends PolicyKey { @TestApi public AccountTypePolicyKey(@NonNull String key, @NonNull String accountType) { super(key); - PolicySizeVerifier.enforceMaxStringLength(accountType, "accountType"); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 00e67e64502a..c993671f4fc1 100644 --- a/core/java/android/app/admin/BundlePolicyValue.java +++ b/core/java/android/app/admin/BundlePolicyValue.java @@ -18,6 +18,7 @@ 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; @@ -30,7 +31,9 @@ public final class BundlePolicyValue extends PolicyValue<Bundle> { public BundlePolicyValue(Bundle value) { super(value); - PolicySizeVerifier.enforceMaxBundleFieldsLength(value); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 f092b7bb5538..a7a2f7d27e0d 100644 --- a/core/java/android/app/admin/ComponentNamePolicyValue.java +++ b/core/java/android/app/admin/ComponentNamePolicyValue.java @@ -18,6 +18,7 @@ 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; @@ -30,7 +31,9 @@ public final class ComponentNamePolicyValue extends PolicyValue<ComponentName> { public ComponentNamePolicyValue(@NonNull ComponentName value) { super(value); - PolicySizeVerifier.enforceMaxComponentNameLength(value); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 c0e435c04d3c..156512a90295 100644 --- a/core/java/android/app/admin/DevicePolicyIdentifiers.java +++ b/core/java/android/app/admin/DevicePolicyIdentifiers.java @@ -16,6 +16,8 @@ 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; @@ -183,11 +185,13 @@ 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 0f54cb7bc35e..d31d8f27844a 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -54,8 +54,10 @@ 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; @@ -10476,6 +10478,10 @@ public class DevicePolicyManager { @WorkerThread public void setApplicationRestrictions(@Nullable ComponentName admin, String packageName, Bundle settings) { + if (!Flags.dmrhSetAppRestrictions()) { + throwIfParentInstance("setApplicationRestrictions"); + } + if (mService != null) { try { mService.setApplicationRestrictions(admin, mContext.getPackageName(), packageName, @@ -11880,6 +11886,9 @@ public class DevicePolicyManager { @WorkerThread public @NonNull Bundle getApplicationRestrictions( @Nullable ComponentName admin, String packageName) { + if (!Flags.dmrhSetAppRestrictions()) { + throwIfParentInstance("getApplicationRestrictions"); + } if (mService != null) { try { @@ -14224,11 +14233,21 @@ public class DevicePolicyManager { */ public @NonNull DevicePolicyManager getParentProfileInstance(@NonNull ComponentName admin) { throwIfParentInstance("getParentProfileInstance"); - UserManager um = mContext.getSystemService(UserManager.class); - if (!um.isManagedProfile()) { - throw new SecurityException("The current user does not have a parent profile."); + try { + if (Flags.dmrhSetAppRestrictions()) { + UserManager um = mContext.getSystemService(UserManager.class); + if (!um.isManagedProfile()) { + throw new SecurityException("The current user does not have a parent profile."); + } + } else { + if (!mService.isManagedProfile(admin)) { + throw new SecurityException("The current user does not have a parent profile."); + } + } + return new DevicePolicyManager(mContext, mService, true); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - return new DevicePolicyManager(mContext, mService, true); } /** @@ -17790,6 +17809,7 @@ 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 { @@ -17807,6 +17827,7 @@ 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 { @@ -17825,9 +17846,13 @@ 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 5f9bb9c22893..f70a53f61671 100644 --- a/core/java/android/app/admin/EnforcingAdmin.java +++ b/core/java/android/app/admin/EnforcingAdmin.java @@ -16,6 +16,9 @@ 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; @@ -61,6 +64,7 @@ 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 ab32d46a05ad..68b4ad84d81a 100644 --- a/core/java/android/app/admin/LockTaskPolicy.java +++ b/core/java/android/app/admin/LockTaskPolicy.java @@ -19,6 +19,7 @@ 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; @@ -134,8 +135,10 @@ public final class LockTaskPolicy extends PolicyValue<LockTaskPolicy> { } private void setPackagesInternal(Set<String> packages) { - for (String p : packages) { - PolicySizeVerifier.enforceMaxPackageNameLength(p); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 226c576d9bc3..1a04f6c908bc 100644 --- a/core/java/android/app/admin/PackagePermissionPolicyKey.java +++ b/core/java/android/app/admin/PackagePermissionPolicyKey.java @@ -25,6 +25,7 @@ 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; @@ -58,8 +59,10 @@ public final class PackagePermissionPolicyKey extends PolicyKey { public PackagePermissionPolicyKey(@NonNull String identifier, @NonNull String packageName, @NonNull String permissionName) { super(identifier); - PolicySizeVerifier.enforceMaxPackageNameLength(packageName); - PolicySizeVerifier.enforceMaxStringLength(permissionName, "permissionName"); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 8fa21dbb0a2e..9e31a23aec91 100644 --- a/core/java/android/app/admin/PackagePolicyKey.java +++ b/core/java/android/app/admin/PackagePolicyKey.java @@ -24,6 +24,7 @@ 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; @@ -54,7 +55,9 @@ public final class PackagePolicyKey extends PolicyKey { @TestApi public PackagePolicyKey(@NonNull String key, @NonNull String packageName) { super(key); - PolicySizeVerifier.enforceMaxPackageNameLength(packageName); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 24c50b0994d7..8b253a23a299 100644 --- a/core/java/android/app/admin/PackageSetPolicyValue.java +++ b/core/java/android/app/admin/PackageSetPolicyValue.java @@ -18,6 +18,7 @@ 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; @@ -31,8 +32,10 @@ public final class PackageSetPolicyValue extends PolicyValue<Set<String>> { public PackageSetPolicyValue(@NonNull Set<String> value) { super(value); - for (String packageName : value) { - PolicySizeVerifier.enforceMaxPackageNameLength(packageName); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 bb07c23163ea..6efe9ad0dbed 100644 --- a/core/java/android/app/admin/StringPolicyValue.java +++ b/core/java/android/app/admin/StringPolicyValue.java @@ -18,6 +18,7 @@ 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; @@ -29,7 +30,9 @@ public final class StringPolicyValue extends PolicyValue<String> { public StringPolicyValue(@NonNull String value) { super(value); - PolicySizeVerifier.enforceMaxStringLength(value, "policyValue"); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 16cfba4414d5..9054287cb7a0 100644 --- a/core/java/android/app/admin/UserRestrictionPolicyKey.java +++ b/core/java/android/app/admin/UserRestrictionPolicyKey.java @@ -21,6 +21,7 @@ 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; @@ -44,7 +45,9 @@ public final class UserRestrictionPolicyKey extends PolicyKey { @TestApi public UserRestrictionPolicyKey(@NonNull String identifier, @NonNull String restriction) { super(identifier); - PolicySizeVerifier.enforceMaxStringLength(restriction, "restriction"); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 e940a7bb96ad..9daf35593108 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -4,7 +4,6 @@ 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 @@ -29,6 +28,16 @@ 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" @@ -68,6 +77,13 @@ 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" @@ -210,6 +226,26 @@ 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)" + bug: "328758346" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "always_persist_do" namespace: "enterprise" description: "Always write device_owners2.xml so that migration flags aren't lost" @@ -227,6 +263,16 @@ 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" @@ -247,6 +293,16 @@ 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" @@ -257,6 +313,16 @@ flag { } flag { + name: "headless_single_user_fixes" + namespace: "enterprise" + description: "Various fixes for headless single user mode" + bug: "289515470" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "backup_connected_apps_settings" namespace: "enterprise" description: "backup and restore connected work and personal apps user settings across devices" 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/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/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/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/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/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/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/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/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/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 e873cbd6341d..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; 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/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 4622dcffb3cc..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; 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..694b1b0c2532 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; 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/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/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/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/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 5b7521a37a6c..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 @@ -22,7 +22,6 @@ import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.FlakyTest import com.android.wm.shell.flicker.pip.common.PipTransition import org.junit.FixMethodOrder import org.junit.Test @@ -36,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 @@ -46,22 +45,6 @@ class PipAspectRatioChangeTest(flicker: LegacyFlickerTest) : PipTransition(flick flicker.assertLayersEnd { this.visibleRegion(pipApp).isSameAspectRatio(1, 2) } } - @FlakyTest(bugId = 358278071) - override fun hasAtMostOnePipDismissOverlayWindow() = - super.hasAtMostOnePipDismissOverlayWindow() - - @FlakyTest(bugId = 358278071) - override fun statusBarLayerPositionAtStartAndEnd() = - super.statusBarLayerPositionAtStartAndEnd() - - @FlakyTest(bugId = 358278071) - override fun taskBarWindowIsAlwaysVisible() = - super.taskBarWindowIsAlwaysVisible() - - @FlakyTest(bugId = 358278071) - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - companion object { /** * Creates the test configurations. 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 6695a1e56567..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 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/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/tests/utils/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlagsKosmos.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl index 60d97d1b1437..1726036f0ded 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/shared/flag/ComposeBouncerFlagsKosmos.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsProviderServiceStatus.aidl @@ -14,9 +14,6 @@ * limitations under the License. */ -package com.android.systemui.bouncer.shared.flag +package com.android.settingslib.bluetooth.devicesettings; -import com.android.systemui.kosmos.Kosmos - -var Kosmos.fakeComposeBouncerFlags by Kosmos.Fixture { FakeComposeBouncerFlags() } -val Kosmos.composeBouncerFlags by Kosmos.Fixture<ComposeBouncerFlags> { fakeComposeBouncerFlags } +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/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/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/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 97206de346c5..1ce171609e5b 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" diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeOverlayModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeOverlayModule.kt new file mode 100644 index 000000000000..bc4adf93c556 --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeOverlayModule.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.qs.ui.composable.QuickSettingsShadeOverlay +import com.android.systemui.scene.ui.composable.Overlay +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet + +@Module +interface QuickSettingsShadeOverlayModule { + + @Binds @IntoSet fun quickSettingsShade(overlay: QuickSettingsShadeOverlay): Overlay +} 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..fa37729d1bbe --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -0,0 +1,85 @@ +/* + * 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.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.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 java.util.Optional +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, + viewModelFactory = viewModel.overlayShadeViewModelFactory, + lockscreenContent = { Optional.empty() }, + ) { + Column { + ExpandedShadeHeader( + viewModelFactory = viewModel.shadeHeaderViewModelFactory, + createTintedIconManager = tintedIconManagerFactory::create, + createBatteryMeterViewController = batteryMeterViewControllerFactory::create, + statusBarIconController = statusBarIconController, + modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding), + ) + + ShadeBody( + viewModel = viewModel.quickSettingsContainerViewModel, + ) + } + } + } +} 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..d8ab0a1ab577 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,7 +100,6 @@ 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 @@ -269,13 +269,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 +302,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 +337,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 +572,7 @@ private fun SceneScope.SplitShade( ShadeMediaCarousel( isVisible = isMediaVisible, + isInRow = false, mediaHost = mediaHost, mediaOffsetProvider = mediaOffsetProvider, modifier = @@ -657,6 +630,7 @@ private fun SceneScope.SplitShade( @Composable private fun SceneScope.ShadeMediaCarousel( isVisible: Boolean, + isInRow: Boolean, mediaHost: MediaHost, carouselController: MediaCarouselController, mediaOffsetProvider: ShadeMediaOffsetProvider, @@ -668,7 +642,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/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/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index e9300119924e..3a8fea76bf91 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 @@ -21,9 +21,15 @@ import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.core.AnimationSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable +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 kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch @Composable internal fun PredictiveBackHandler( @@ -42,10 +48,11 @@ internal fun 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. @@ -64,7 +71,8 @@ private suspend fun <T : ContentKey> animate( ) { fun animateOffset(targetContent: T, spec: AnimationSpec<Float>? = null) { if ( - layoutImpl.state.transitionState != animation.contentTransition || animation.isFinishing + layoutImpl.state.transitionState != animation.contentTransition || + animation.isAnimatingOffset() ) { return } @@ -76,20 +84,34 @@ private suspend fun <T : ContentKey> animate( ) } - layoutImpl.state.startTransition(animation.contentTransition) - try { - progress.collect { backEvent -> animation.dragOffset = backEvent.progress } + coroutineScope { + launch { + try { + progress.collect { backEvent -> animation.dragOffset = backEvent.progress } - // Back gesture successful. - animateOffset( - animation.toContent, - animation.contentTransition.transformationSpec.progressSpec - ) - } catch (e: CancellationException) { - // Back gesture cancelled. - // If the back gesture is cancelled, the progress is animated back to 0f by the system. - // Since the remaining change in progress is usually very small, the progressSpec is omitted - // and the default spring spec used instead. - animateOffset(animation.fromContent) + // Back gesture successful. + animateOffset( + animation.toContent, + animation.contentTransition.transformationSpec.progressSpec, + ) + } catch (e: CancellationException) { + // Back gesture cancelled. + animateOffset(animation.fromContent) + } + } + + // Start the transition. + layoutImpl.state.startTransition(animation.contentTransition) + } +} + +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..4b3676859be4 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]. */ @@ -617,7 +606,7 @@ internal fun SceneTransitionLayoutForTesting( swipeSourceDetector = swipeSourceDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = builder, - coroutineScope = coroutineScope, + animationScope = coroutineScope, ) .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..cc7d146b8c70 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,25 @@ 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 [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)`. */ fun setTargetScene( targetScene: SceneKey, + // TODO(b/362727477): Rename to animationScope. coroutineScope: CoroutineScope, transitionKey: TransitionKey? = null, - ): TransitionState.Transition? + ): Pair<TransitionState.Transition, Job>? /** Immediately snap to the given [scene]. */ fun snapToScene( @@ -299,7 +304,7 @@ internal class MutableSceneTransitionLayoutStateImpl( targetScene: SceneKey, coroutineScope: CoroutineScope, transitionKey: TransitionKey?, - ): TransitionState.Transition.ChangeScene? { + ): Pair<TransitionState.Transition.ChangeScene, Job>? { checkThread() return coroutineScope.animateToScene( @@ -310,17 +315,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 +404,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 +418,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 +473,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 +493,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 +507,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 +515,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 be9c5670ceae..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 @@ -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: AnimationSpec<Float>? = null, - ): OffsetAnimation { + ) { + 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/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/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..7498df134bba 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 @@ -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. + coroutineScope = 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. + coroutineScope = 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..2c723eca183d 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 @@ -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. 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..29eedf6dd3c7 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,14 @@ 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.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 kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin 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 +58,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() @@ -79,11 +82,10 @@ class SceneTransitionLayoutStateTest { @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, coroutineScope = this)) assertThat(state.transitionState).isEqualTo(transition) - transition!!.finish().join() + job.join() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) } @@ -91,11 +93,10 @@ class SceneTransitionLayoutStateTest { fun setTargetScene_transitionToSameScene() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutState(SceneA) - val transition = state.setTargetScene(SceneB, coroutineScope = this) - assertThat(transition).isNotNull() + val (_, job) = checkNotNull(state.setTargetScene(SceneB, coroutineScope = this)) assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNull() - transition!!.finish().join() + job.join() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) } @@ -104,10 +105,9 @@ class SceneTransitionLayoutStateTest { val state = MutableSceneTransitionLayoutState(SceneA) assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() - val transition = state.setTargetScene(SceneC, coroutineScope = this) - assertThat(transition).isNotNull() + val (_, job) = checkNotNull(state.setTargetScene(SceneC, coroutineScope = this)) - transition!!.finish().join() + job.join() assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneC)) } @@ -118,7 +118,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, coroutineScope = this)).first } assertThat(state.transitionState).isEqualTo(transition) @@ -127,18 +127,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 +151,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 +194,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 +222,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 +256,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) } @@ -321,7 +309,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 +328,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 +348,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 +371,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 +393,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 +446,8 @@ class SceneTransitionLayoutStateTest { SceneA, sceneTransitions, ) - state.startTransition( + state.startTransitionImmediately( + animationScope = backgroundScope, transition( from = SceneA, to = SceneB, @@ -560,54 +552,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 +609,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 @@ -650,4 +643,21 @@ 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) + } } 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/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/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 15c5e2485a45..fabc357c2a68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -18,8 +18,10 @@ package com.android.keyguard import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags import android.content.res.Configuration import android.media.AudioManager +import android.platform.test.annotations.EnableFlags import android.telephony.TelephonyManager import android.testing.TestableLooper.RunWithLooper import android.testing.TestableResources @@ -938,6 +940,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { } @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() { val mainUserId = 10 @@ -954,6 +957,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { } @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() { val secondaryUserId = 10 val mainUserId = 0 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/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 080b48af2af1..0c5e726e17aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.authentication.domain.interactor import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils @@ -412,6 +414,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) fun upcomingWipe() = testScope.runTest { val upcomingWipe by collectLastValue(underTest.upcomingWipe) 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/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/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/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt new file mode 100644 index 000000000000..fbfefb9bffcf --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelTest.kt @@ -0,0 +1,83 @@ +/* + * 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.shade.data.repository.fakeShadeRepository +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 fakeShadeRepository by lazy { kosmos.fakeShadeRepository } + + private val underTest by lazy { kosmos.quickSettingsShadeOverlayActionsViewModel } + + @Test + fun upTransitionSceneKey_topAligned_hidesShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + fakeShadeRepository.setDualShadeAlignedToBottom(false) + underTest.activateIn(this) + + assertThat((actions?.get(Swipe.Up) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.QuickSettingsShade) + assertThat(actions?.get(Swipe.Down)).isNull() + } + + @Test + fun upTransitionSceneKey_bottomAligned_doesNothing() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + fakeShadeRepository.setDualShadeAlignedToBottom(true) + underTest.activateIn(this) + + assertThat(actions?.get(Swipe.Up)).isNull() + assertThat((actions?.get(Swipe.Down) as? UserActionResult.HideOverlay)?.overlay) + .isEqualTo(Overlays.QuickSettingsShade) + } + + @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/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 1307301774c8..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"> 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/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 2d28a189f84d..61f9800c351b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -35,6 +35,7 @@ import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -1139,7 +1140,12 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard int remainingBeforeWipe, int failedAttempts) { int userType = USER_TYPE_PRIMARY; if (expiringUserId == userId) { - int primaryUser = mainUserId != null ? mainUserId : UserHandle.USER_SYSTEM; + int primaryUser = UserHandle.USER_SYSTEM; + if (Flags.headlessSingleUserFixes()) { + if (mainUserId != null) { + primaryUser = mainUserId; + } + } // TODO: http://b/23522538 if (expiringUserId != primaryUser) { userType = USER_TYPE_SECONDARY_USER; 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/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/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 3080e1978b2a..fcba425f0956 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.authentication.domain.interactor +import android.app.admin.flags.Flags import android.os.UserHandle import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternView @@ -288,7 +289,12 @@ constructor( private suspend fun getWipeTarget(): WipeTarget { // Check which profile has the strictest policy for failed authentication attempts. val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe() - val primaryUser = selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM + val primaryUser = + if (Flags.headlessSingleUserFixes()) { + selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM + } else { + UserHandle.USER_SYSTEM + } return when (userToBeWiped) { selectedUserInteractor.getSelectedUserId() -> if (userToBeWiped == primaryUser) { 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/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/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/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/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/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.kt new file mode 100644 index 000000000000..b75f180afe9b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModel.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.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.shared.model.TransitionKeys +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 for navigating to other scenes or overlays. */ +class QuickSettingsShadeOverlayActionsViewModel +@AssistedInject +constructor( + private val shadeInteractor: ShadeInteractor, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + setActions( + buildMap { + if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) { + put(Swipe.Up, UserActionResult.HideOverlay(Overlays.QuickSettingsShade)) + } else { + put( + Swipe.Down, + UserActionResult.HideOverlay( + overlay = Overlays.QuickSettingsShade, + transitionKey = TransitionKeys.OpenBottomShade, + ) + ) + } + 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..b8311cef44a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt @@ -0,0 +1,42 @@ +/* + * 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.shade.ui.viewmodel.OverlayShadeViewModel +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 overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, + val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory, + val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, +) { + + @AssistedFactory + interface Factory { + fun create(): QuickSettingsShadeOverlayContentViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index 98cf941d2c85..00944b8d0849 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -46,6 +46,7 @@ import dagger.multibindings.IntoMap NotificationsShadeOverlayModule::class, NotificationsShadeSceneModule::class, NotificationsShadeSessionModule::class, + QuickSettingsShadeOverlayModule::class, QuickSettingsSceneModule::class, ShadeSceneModule::class, SceneDomainModule::class, @@ -104,6 +105,7 @@ interface KeyguardlessSceneContainerFrameworkModule { overlayKeys = listOfNotNull( Overlays.NotificationsShade.takeIf { DualShade.isEnabled }, + Overlays.QuickSettingsShade.takeIf { DualShade.isEnabled }, ), navigationDistances = mapOf( diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 86b242782308..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 @@ -44,12 +43,12 @@ 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, @@ -113,6 +112,7 @@ interface SceneContainerFrameworkModule { overlayKeys = listOfNotNull( Overlays.NotificationsShade.takeIf { DualShade.isEnabled }, + Overlays.QuickSettingsShade.takeIf { DualShade.isEnabled }, ), navigationDistances = mapOf( 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 index 0bb02e92c9ba..c47a85082032 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Overlays.kt @@ -36,4 +36,17 @@ object Overlays { * 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/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/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/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 64d71240073f..036e21c06795 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; } @@ -1998,7 +1997,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 +2006,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 +2031,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 +2629,9 @@ public class NotificationStackScrollLayout } private void updateScrollability() { + if (SceneContainerFlag.isEnabled()) { + return; + } boolean scrollable = !mQsFullScreen && getScrollRange() > 0; if (scrollable != mScrollable) { mScrollable = scrollable; @@ -2633,6 +2641,7 @@ public class NotificationStackScrollLayout } private void updateForwardAndBackwardScrollability() { + SceneContainerFlag.assertInLegacyMode(); boolean forwardScrollable = mScrollable && !mScrollAdapter.isScrolledToBottom(); boolean backwardsScrollable = mScrollable && !mScrollAdapter.isScrolledToTop(); boolean changed = forwardScrollable != mForwardScrollable @@ -4172,6 +4181,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 +4947,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 +4961,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 +5151,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 +5200,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/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/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/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/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/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelKosmos.kt new file mode 100644 index 000000000000..41ca2f9754e1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayActionsViewModelKosmos.kt @@ -0,0 +1,28 @@ +/* + * 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.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.quickSettingsShadeOverlayActionsViewModel: + QuickSettingsShadeOverlayActionsViewModel by Fixture { + QuickSettingsShadeOverlayActionsViewModel( + shadeInteractor = shadeInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt new file mode 100644 index 000000000000..9025c5c22d0e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt @@ -0,0 +1,30 @@ +/* + * 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.kosmos.Kosmos +import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModelFactory +import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory + +val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by + Kosmos.Fixture { + QuickSettingsShadeOverlayContentViewModel( + overlayShadeViewModelFactory = overlayShadeViewModelFactory, + shadeHeaderViewModelFactory = shadeHeaderViewModelFactory, + 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 7bc2483961bf..dc45d939ac1b 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 @@ -28,6 +28,7 @@ val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen } var Kosmos.overlayKeys by Fixture { listOf( Overlays.NotificationsShade, + Overlays.QuickSettingsShade, ) } 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/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/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/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/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/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 047eb29efcc0..cb3de73373ae 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; @@ -861,7 +861,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 +935,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 @@ -2628,6 +2628,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); @@ -2702,25 +2704,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); } @@ -2732,24 +2748,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); } } 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 8ee708562565..d00ab83b2ec6 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 @@ -214,6 +214,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); 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/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..5e35925aa69e 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); } 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/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index b982098fefa4..5eec0124a9e3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -1325,6 +1325,11 @@ 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 4beb6a8a3480..a08af72586ee 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -230,9 +230,11 @@ final class DevicePolicyEngine { synchronized (mLock) { PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId); - if (!handleAdminPolicySizeLimit(localPolicyState, enforcingAdmin, value, - policyDefinition, userId)) { - return; + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + if (!handleAdminPolicySizeLimit(localPolicyState, enforcingAdmin, value, + policyDefinition, userId)) { + return; + } } if (policyDefinition.isNonCoexistablePolicy()) { @@ -352,7 +354,9 @@ final class DevicePolicyEngine { } PolicyState<V> localPolicyState = getLocalPolicyStateLocked(policyDefinition, userId); - decreasePolicySizeForAdmin(localPolicyState, enforcingAdmin); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + decreasePolicySizeForAdmin(localPolicyState, enforcingAdmin); + } if (policyDefinition.isNonCoexistablePolicy()) { setNonCoexistableLocalPolicyLocked(policyDefinition, localPolicyState, @@ -496,9 +500,11 @@ final class DevicePolicyEngine { synchronized (mLock) { PolicyState<V> globalPolicyState = getGlobalPolicyStateLocked(policyDefinition); - if (!handleAdminPolicySizeLimit(globalPolicyState, enforcingAdmin, value, - policyDefinition, UserHandle.USER_ALL)) { - return; + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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 @@ -565,7 +571,9 @@ final class DevicePolicyEngine { synchronized (mLock) { PolicyState<V> policyState = getGlobalPolicyStateLocked(policyDefinition); - decreasePolicySizeForAdmin(policyState, enforcingAdmin); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + decreasePolicySizeForAdmin(policyState, enforcingAdmin); + } boolean policyChanged = policyState.removePolicy(enforcingAdmin); @@ -1731,23 +1739,25 @@ final class DevicePolicyEngine { pw.println(); } pw.decreaseIndent(); - pw.println(); + if (Flags.devicePolicySizeTrackingInternalBugFixEnabled()) { + 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.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(); } - pw.decreaseIndent(); } } @@ -2008,21 +2018,23 @@ final class DevicePolicyEngine { private void writeEnforcingAdminSizeInner(TypedXmlSerializer serializer) throws IOException { - 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 (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); + } } } } @@ -2030,6 +2042,9 @@ 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); @@ -2177,6 +2192,9 @@ 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 470025a67dee..4bc0ef994b60 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1328,7 +1328,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Bundle prevRestrictions) { resetCrossProfileIntentFiltersIfNeeded(userId, newRestrictions, prevRestrictions); resetUserVpnIfNeeded(userId, newRestrictions, prevRestrictions); - removePrivateSpaceIfRestrictionIsSet(userId, newRestrictions, prevRestrictions); + if (Flags.deletePrivateSpaceUnderRestriction()) { + removePrivateSpaceIfRestrictionIsSet(userId, newRestrictions, prevRestrictions); + } } private void resetUserVpnIfNeeded( @@ -3693,6 +3695,9 @@ 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. @@ -7151,7 +7156,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // If there is a profile owner, redirect to that; otherwise query the device owner. ComponentName aliasChooser = getProfileOwnerAsUser(caller.getUserId()); - boolean isDoUser = caller.getUserId() == getDeviceOwnerUserId(); + boolean isDoUser = Flags.headlessSingleUserFixes() + ? caller.getUserId() == getDeviceOwnerUserId() + : caller.getUserHandle().isSystem(); if (aliasChooser == null && isDoUser) { synchronized (getLockObject()) { final ActiveAdmin deviceOwnerAdmin = getDeviceOwnerAdminLocked(); @@ -8161,7 +8168,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // First check whether the admin is allowed to wipe the device/user/profile. final String restriction; boolean shouldFactoryReset = userId == UserHandle.USER_SYSTEM; - if (getHeadlessDeviceOwnerModeForDeviceOwner() + if (Flags.headlessSingleUserFixes() && getHeadlessDeviceOwnerModeForDeviceOwner() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER) { shouldFactoryReset = userId == getMainUserId(); } @@ -8185,7 +8192,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { adminPackage, userId)) { // Legacy mode - wipeDevice = getHeadlessDeviceOwnerModeForDeviceOwner() + wipeDevice = Flags.headlessSingleUserFixes() + && getHeadlessDeviceOwnerModeForDeviceOwner() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER ? isMainUser : isSystemUser; } else { // Explicit behaviour @@ -9369,7 +9377,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { void sendDeviceOwnerOrProfileOwnerCommand(String action, Bundle extras, int userId) { if (userId == UserHandle.USER_ALL) { - if (getHeadlessDeviceOwnerModeForDeviceOwner() + if (Flags.headlessDeviceOwnerDelegateSecurityLoggingBugFix() + && getHeadlessDeviceOwnerModeForDeviceOwner() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER) { userId = mOwners.getDeviceOwnerUserId(); } else { @@ -11855,7 +11864,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } setBackwardsCompatibleAppRestrictions( caller, packageName, restrictions, caller.getUserHandle()); - } else { + } else if (Flags.dmrhSetAppRestrictions()) { final boolean isRoleHolder; if (who != null) { // DO or PO @@ -11902,6 +11911,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller.getUserHandle()); }); } + } else { + Preconditions.checkCallAuthorization((caller.hasAdminComponent() + && (isProfileOwner(caller) || isDefaultDeviceOwner(caller))) + || (caller.hasPackage() && isCallerDelegate(caller, + DELEGATION_APP_RESTRICTIONS))); + mInjector.binderWithCleanCallingIdentity(() -> { + mUserManager.setApplicationRestrictions(packageName, restrictions, + caller.getUserHandle()); + }); } DevicePolicyEventLogger @@ -12434,6 +12452,12 @@ 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) { @@ -13232,7 +13256,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return Bundle.EMPTY; } return policies.get(enforcingAdmin).getValue(); - } else { + } else if (Flags.dmrhSetAppRestrictions()) { final boolean isRoleHolder; if (who != null) { // Caller is DO or PO. They cannot call this on parent @@ -13275,6 +13299,19 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return bundle != null ? bundle : Bundle.EMPTY; }); } + + } else { + Preconditions.checkCallAuthorization((caller.hasAdminComponent() + && (isProfileOwner(caller) || isDefaultDeviceOwner(caller))) + || (caller.hasPackage() && isCallerDelegate(caller, + DELEGATION_APP_RESTRICTIONS))); + return mInjector.binderWithCleanCallingIdentity(() -> { + Bundle bundle = mUserManager.getApplicationRestrictions(packageName, + caller.getUserHandle()); + // if no restrictions were saved, mUserManager.getApplicationRestrictions + // returns null, but DPM method should return an empty Bundle as per JavaDoc + return bundle != null ? bundle : Bundle.EMPTY; + }); } } @@ -14283,6 +14320,10 @@ 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); @@ -14893,6 +14934,11 @@ 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); @@ -16776,11 +16822,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); } - final UserHandle user = UserHandle.of(userId); - final String roleHolderPackage = getRoleHolderPackageNameOnUser( - RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT, userId); - if (roleHolderPackage != null) { - broadcastExplicitIntentToPackage(intent, roleHolderPackage, user); + 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); + } } } }); @@ -16788,10 +16836,18 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public SystemUpdateInfo getPendingSystemUpdate(ComponentName admin, String callerPackage) { - CallerIdentity caller = getCallerIdentity(admin, callerPackage); - enforcePermissions(new String[] {NOTIFY_PENDING_SYSTEM_UPDATE, - MANAGE_DEVICE_POLICY_QUERY_SYSTEM_UPDATES}, caller.getPackageName(), - caller.getUserId()); + 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)); + } return mOwners.getSystemUpdateInfo(); } @@ -17335,10 +17391,17 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Nullable ComponentName componentName, @UserIdInt int callingUserId) { synchronized (getLockObject()) { int deviceOwnerUserId = -1; - deviceOwnerUserId = mInjector.userManagerIsHeadlessSystemUserMode() - && getHeadlessDeviceOwnerModeForDeviceAdmin(componentName, callingUserId) - == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED - ? UserHandle.USER_SYSTEM : callingUserId; + 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; + } 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. @@ -18637,7 +18700,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Backup service has to be enabled on the main user in order for it to be enabled on // secondary users. - if (isDeviceOwner(caller) && getHeadlessDeviceOwnerModeForDeviceOwner() + if (Flags.headlessSingleUserFixes() && isDeviceOwner(caller) + && getHeadlessDeviceOwnerModeForDeviceOwner() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER) { toggleBackupServiceActive(UserHandle.USER_SYSTEM, enabled); } @@ -21378,7 +21442,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final CallerIdentity caller = getCallerIdentity(callerPackage); - enforcePermission(MANAGE_DEVICE_POLICY_CERTIFICATES, caller.getPackageName()); + if (Flags.permissionMigrationForZeroTrustImplEnabled()) { + enforcePermission(MANAGE_DEVICE_POLICY_CERTIFICATES, caller.getPackageName()); + } else { + Preconditions.checkCallAuthorization( + isDefaultDeviceOwner(caller) || isProfileOwner(caller) + || isCallerDelegate(caller, DELEGATION_CERT_INSTALL)); + } synchronized (getLockObject()) { final ActiveAdmin requiredAdmin = getDeviceOrProfileOwnerAdminLocked( caller.getUserId()); @@ -21977,9 +22047,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final long identity = Binder.clearCallingIdentity(); try { boolean isSingleUserMode; - int headlessDeviceOwnerMode = getHeadlessDeviceOwnerModeForDeviceAdmin( - deviceAdmin, caller.getUserId()); - isSingleUserMode = headlessDeviceOwnerMode == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; + 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; + } if (Flags.headlessSingleMinTargetSdk() && mInjector.userManagerIsHeadlessSystemUserMode() @@ -22378,17 +22455,35 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Objects.requireNonNull(packageName, "Admin package name must be provided"); final CallerIdentity caller = getCallerIdentity(packageName); - synchronized (getLockObject()) { - EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin( - /* admin= */ null, MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING, - caller.getPackageName(), - caller.getUserId()); + 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."); Preconditions.checkState(canUsbDataSignalingBeDisabled(), "USB data signaling cannot be disabled."); - mDevicePolicyEngine.setGlobalPolicy( - PolicyDefinition.USB_DATA_SIGNALING, - enforcingAdmin, - new BooleanPolicyValue(enabled)); + } + + 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()); + } + } } DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_USB_DATA_SIGNALING) @@ -22410,10 +22505,24 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public boolean isUsbDataSignalingEnabled(String packageName) { final CallerIdentity caller = getCallerIdentity(packageName); - Boolean enabled = mDevicePolicyEngine.getResolvedPolicy( - PolicyDefinition.USB_DATA_SIGNALING, - caller.getUserId()); - return enabled == null || enabled; + 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(); + } + } + } } private boolean isUsbDataSignalingEnabledInternalLocked() { @@ -24766,6 +24875,9 @@ 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()); @@ -24779,6 +24891,9 @@ 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()); @@ -24788,6 +24903,9 @@ 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()); @@ -24798,6 +24916,9 @@ 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/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/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/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/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/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) { |