diff options
349 files changed, 6579 insertions, 3324 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index edb119e0afb1..6f8a189c42b8 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -438,10 +438,23 @@ aconfig_declarations { name: "android.companion.virtualdevice.flags-aconfig", package: "android.companion.virtualdevice.flags", container: "system", + exportable: true, srcs: ["core/java/android/companion/virtual/flags/*.aconfig"], } java_aconfig_library { + name: "android.companion.virtualdevice.flags-aconfig-java-export", + aconfig_declarations: "android.companion.virtualdevice.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", + min_sdk_version: "30", + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], +} + +java_aconfig_library { name: "android.companion.virtual.flags-aconfig-java", aconfig_declarations: "android.companion.virtual.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index d92351de3aa1..c9d340757c6b 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -1989,10 +1989,8 @@ public class AppStandbyController mAdminProtectedPackages.put(userId, packageNames); } } - if (android.app.admin.flags.Flags.disallowUserControlBgUsageFix()) { - if (!Flags.avoidIdleCheck() || mInjector.getBootPhase() >= PHASE_BOOT_COMPLETED) { - postCheckIdleStates(userId); - } + if (!Flags.avoidIdleCheck() || mInjector.getBootPhase() >= PHASE_BOOT_COMPLETED) { + postCheckIdleStates(userId); } } diff --git a/core/api/current.txt b/core/api/current.txt index 861be4079acc..ed8ab06810f9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -141,7 +141,7 @@ package android { field public static final String MANAGE_DEVICE_POLICY_APPS_CONTROL = "android.permission.MANAGE_DEVICE_POLICY_APPS_CONTROL"; field public static final String MANAGE_DEVICE_POLICY_APP_RESTRICTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_RESTRICTIONS"; field public static final String MANAGE_DEVICE_POLICY_APP_USER_DATA = "android.permission.MANAGE_DEVICE_POLICY_APP_USER_DATA"; - field @FlaggedApi("android.app.admin.flags.assist_content_user_restriction_enabled") public static final String MANAGE_DEVICE_POLICY_ASSIST_CONTENT = "android.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT"; + field public static final String MANAGE_DEVICE_POLICY_ASSIST_CONTENT = "android.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT"; field public static final String MANAGE_DEVICE_POLICY_AUDIO_OUTPUT = "android.permission.MANAGE_DEVICE_POLICY_AUDIO_OUTPUT"; field public static final String MANAGE_DEVICE_POLICY_AUTOFILL = "android.permission.MANAGE_DEVICE_POLICY_AUTOFILL"; field public static final String MANAGE_DEVICE_POLICY_BACKUP_SERVICE = "android.permission.MANAGE_DEVICE_POLICY_BACKUP_SERVICE"; @@ -169,7 +169,7 @@ package android { field public static final String MANAGE_DEVICE_POLICY_LOCK = "android.permission.MANAGE_DEVICE_POLICY_LOCK"; field public static final String MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS = "android.permission.MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS"; field public static final String MANAGE_DEVICE_POLICY_LOCK_TASK = "android.permission.MANAGE_DEVICE_POLICY_LOCK_TASK"; - field @FlaggedApi("android.app.admin.flags.esim_management_enabled") public static final String MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS = "android.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS"; + field public static final String MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS = "android.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS"; field public static final String MANAGE_DEVICE_POLICY_METERED_DATA = "android.permission.MANAGE_DEVICE_POLICY_METERED_DATA"; field public static final String MANAGE_DEVICE_POLICY_MICROPHONE = "android.permission.MANAGE_DEVICE_POLICY_MICROPHONE"; field @FlaggedApi("android.app.admin.flags.dedicated_device_control_api_enabled") public static final String MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE = "android.permission.MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE"; @@ -7875,7 +7875,7 @@ package android.app.admin { method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.admin.DeviceAdminInfo> CREATOR; field public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1; // 0x1 - field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; // 0x2 + field public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; // 0x2 field public static final int HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED = 0; // 0x0 field public static final int USES_ENCRYPTED_STORAGE = 7; // 0x7 field public static final int USES_POLICY_DISABLE_CAMERA = 8; // 0x8 @@ -8081,7 +8081,7 @@ package android.app.admin { method public CharSequence getStartUserSessionMessage(@NonNull android.content.ComponentName); method @Deprecated public boolean getStorageEncryption(@Nullable android.content.ComponentName); method public int getStorageEncryptionStatus(); - method @FlaggedApi("android.app.admin.flags.esim_management_enabled") @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS) public java.util.Set<java.lang.Integer> getSubscriptionIds(); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS) public java.util.Set<java.lang.Integer> getSubscriptionIds(); method @Nullable public android.app.admin.SystemUpdatePolicy getSystemUpdatePolicy(); method @Nullable public android.os.PersistableBundle getTransferOwnershipBundle(); method @Nullable public java.util.List<android.os.PersistableBundle> getTrustAgentConfiguration(@Nullable android.content.ComponentName, @NonNull android.content.ComponentName); @@ -34116,7 +34116,7 @@ package android.os { field public static final String DISALLOW_AIRPLANE_MODE = "no_airplane_mode"; field public static final String DISALLOW_AMBIENT_DISPLAY = "no_ambient_display"; field public static final String DISALLOW_APPS_CONTROL = "no_control_apps"; - field @FlaggedApi("android.app.admin.flags.assist_content_user_restriction_enabled") public static final String DISALLOW_ASSIST_CONTENT = "no_assist_content"; + field public static final String DISALLOW_ASSIST_CONTENT = "no_assist_content"; field public static final String DISALLOW_AUTOFILL = "no_autofill"; field public static final String DISALLOW_BLUETOOTH = "no_bluetooth"; field public static final String DISALLOW_BLUETOOTH_SHARING = "no_bluetooth_sharing"; @@ -34166,7 +34166,7 @@ package android.os { field public static final String DISALLOW_SHARE_INTO_MANAGED_PROFILE = "no_sharing_into_profile"; field public static final String DISALLOW_SHARE_LOCATION = "no_share_location"; field public static final String DISALLOW_SHARING_ADMIN_CONFIGURED_WIFI = "no_sharing_admin_configured_wifi"; - field @FlaggedApi("android.app.admin.flags.esim_management_enabled") public static final String DISALLOW_SIM_GLOBALLY = "no_sim_globally"; + field public static final String DISALLOW_SIM_GLOBALLY = "no_sim_globally"; field public static final String DISALLOW_SMS = "no_sms"; field public static final String DISALLOW_SYSTEM_ERROR_DIALOGS = "no_system_error_dialogs"; field @FlaggedApi("com.android.net.thread.platform.flags.thread_user_restriction_enabled") public static final String DISALLOW_THREAD_NETWORK = "no_thread_network"; @@ -43968,8 +43968,11 @@ package android.telephony { field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = "satellite_entitlement_status_refresh_days_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = "satellite_entitlement_supported_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ESOS_INACTIVITY_TIMEOUT_SEC_INT = "satellite_esos_inactivity_timeout_sec_int"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ESOS_SUPPORTED_BOOL = "satellite_esos_supported_bool"; - field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT = "satellite_screen_off_inactivity_timeout_duration_sec_int"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT = "satellite_p2p_sms_inactivity_timeout_sec_int"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_ROAMING_P2P_SMS_SUPPORTED_BOOL = "satellite_roaming_p2p_sms_supported_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_roaming_nb_iot_ntn") public static final String KEY_SATELLITE_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT = "satellite_screen_off_inactivity_timeout_sec_int"; field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool"; field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool"; field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e87bc50013fe..f26522b9b919 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1422,7 +1422,7 @@ package android.app.admin { field public static final int STATUS_DEVICE_ADMIN_NOT_SUPPORTED = 13; // 0xd field public static final int STATUS_HAS_DEVICE_OWNER = 1; // 0x1 field public static final int STATUS_HAS_PAIRED = 8; // 0x8 - field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17; // 0x11 + field public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17; // 0x11 field public static final int STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED = 16; // 0x10 field public static final int STATUS_MANAGED_USERS_NOT_SUPPORTED = 9; // 0x9 field public static final int STATUS_NONSYSTEM_USER_EXISTS = 5; // 0x5 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 0a35c5a36325..009d08245da2 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3138,6 +3138,7 @@ package android.service.dreams { public abstract class DreamOverlayService extends android.app.Service { ctor public DreamOverlayService(); + method @FlaggedApi("android.service.dreams.publish_preview_state_to_overlay") public final boolean isDreamInPreviewMode(); method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); method public void onEndDream(); method public abstract void onStartDream(@NonNull android.view.WindowManager.LayoutParams); diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 14195c473c6d..63e039143917 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -1326,6 +1326,7 @@ public class StatusBarManager { private boolean mClock; private boolean mNotificationIcons; private boolean mRotationSuggestion; + private boolean mQuickSettings; /** @hide */ public DisableInfo(int flags1, int flags2) { @@ -1338,6 +1339,7 @@ public class StatusBarManager { mClock = (flags1 & DISABLE_CLOCK) != 0; mNotificationIcons = (flags1 & DISABLE_NOTIFICATION_ICONS) != 0; mRotationSuggestion = (flags2 & DISABLE2_ROTATE_SUGGESTIONS) != 0; + mQuickSettings = (flags2 & DISABLE2_QUICK_SETTINGS) != 0; } /** @hide */ @@ -1471,6 +1473,20 @@ public class StatusBarManager { } /** + * @hide + */ + public void setQuickSettingsDisabled(boolean disabled) { + mQuickSettings = disabled; + } + + /** + * @hide + */ + public boolean isQuickSettingsDisabled() { + return mQuickSettings; + } + + /** * @return {@code true} if no components are disabled (default state) * @hide */ @@ -1478,7 +1494,7 @@ public class StatusBarManager { public boolean areAllComponentsEnabled() { return !mStatusBarExpansion && !mNavigateHome && !mNotificationPeeking && !mRecents && !mSearch && !mSystemIcons && !mClock && !mNotificationIcons - && !mRotationSuggestion; + && !mRotationSuggestion && !mQuickSettings; } /** @hide */ @@ -1492,6 +1508,7 @@ public class StatusBarManager { mClock = false; mNotificationIcons = false; mRotationSuggestion = false; + mQuickSettings = false; } /** @@ -1502,7 +1519,7 @@ public class StatusBarManager { public boolean areAllComponentsDisabled() { return mStatusBarExpansion && mNavigateHome && mNotificationPeeking && mRecents && mSearch && mSystemIcons && mClock && mNotificationIcons - && mRotationSuggestion; + && mRotationSuggestion && mQuickSettings; } /** @hide */ @@ -1516,6 +1533,7 @@ public class StatusBarManager { mClock = true; mNotificationIcons = true; mRotationSuggestion = true; + mQuickSettings = true; } @NonNull @@ -1533,6 +1551,7 @@ public class StatusBarManager { sb.append(" mClock=").append(mClock ? "disabled" : "enabled"); sb.append(" mNotificationIcons=").append(mNotificationIcons ? "disabled" : "enabled"); sb.append(" mRotationSuggestion=").append(mRotationSuggestion ? "disabled" : "enabled"); + sb.append(" mQuickSettings=").append(mQuickSettings ? "disabled" : "enabled"); return sb.toString(); @@ -1557,6 +1576,7 @@ public class StatusBarManager { if (mClock) disable1 |= DISABLE_CLOCK; if (mNotificationIcons) disable1 |= DISABLE_NOTIFICATION_ICONS; if (mRotationSuggestion) disable2 |= DISABLE2_ROTATE_SUGGESTIONS; + if (mQuickSettings) disable2 |= DISABLE2_QUICK_SETTINGS; return new Pair<Integer, Integer>(disable1, disable2); } diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 46c9e781bed1..4f2efa493a1b 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -16,9 +16,6 @@ package android.app.admin; -import static android.app.admin.flags.Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED; - -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.app.admin.flags.Flags; @@ -195,7 +192,6 @@ public final class DeviceAdminInfo implements Parcelable { * DPCs should set the value of attribute "headless-device-owner-mode" inside the * "headless-system-user" tag as "single_user". */ - @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED) public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; /** diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 5088ea6b603c..d31d8f27844a 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -56,10 +56,8 @@ 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_ESIM_MANAGEMENT_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.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_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; @@ -2988,7 +2986,6 @@ public class DevicePolicyManager { * @hide */ @SystemApi - @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED) public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17; /** @@ -17744,7 +17741,6 @@ public class DevicePolicyManager { * @throws SecurityException if the caller is not authorized to call this method. * @return ids of all managed subscriptions currently downloaded by an admin on the device. */ - @FlaggedApi(FLAG_ESIM_MANAGEMENT_ENABLED) @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS) @NonNull public Set<Integer> getSubscriptionIds() { diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 56f47922b078..29a5048daae6 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -125,13 +125,6 @@ flag { } flag { - name: "dumpsys_policy_engine_migration_enabled" - namespace: "enterprise" - description: "Update DumpSys to include information about migrated APIs in DPE" - bug: "304999634" -} - -flag { name: "allow_querying_profile_type" is_exported: true namespace: "enterprise" @@ -146,6 +139,7 @@ flag { bug: "293441361" } +# Fully rolled out and must not be used. flag { name: "assist_content_user_restriction_enabled" is_exported: true @@ -172,6 +166,7 @@ flag { bug: "304999634" } +# Fully rolled out and must not be used. flag { name: "esim_management_enabled" is_exported: true @@ -180,6 +175,7 @@ flag { bug: "295301164" } +# Fully rolled out and must not be used. flag { name: "headless_device_owner_single_user_enabled" is_exported: true @@ -207,26 +203,6 @@ flag { } flag { - name: "power_exemption_bg_usage_fix" - namespace: "enterprise" - description: "Ensure aps with EXEMPT_FROM_POWER_RESTRICTIONS can execute in the background" - bug: "333379020" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - name: "disallow_user_control_bg_usage_fix" - namespace: "enterprise" - description: "Make DPM.setUserControlDisabledPackages() ensure background usage is allowed" - bug: "326031059" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "disallow_user_control_stopped_state_fix" namespace: "enterprise" description: "Ensure DPM.setUserControlDisabledPackages() clears FLAG_STOPPED for the app" diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index b4c36e1bc513..22a9ccf425c2 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -50,6 +50,7 @@ flag { name: "activity_control_api" description: "Enable APIs for fine grained activity policy, fallback and callbacks" bug: "333443509" + is_exported: true } flag { diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java index 7fcfbbcad950..d7a517a2c067 100644 --- a/core/java/android/content/AttributionSource.java +++ b/core/java/android/content/AttributionSource.java @@ -655,6 +655,7 @@ public final class AttributionSource implements Parcelable { mAttributionSourceState.token = current.getToken(); mAttributionSourceState.renouncedPermissions = current.mAttributionSourceState.renouncedPermissions; + mBuilderFieldsSet |= 0x2 | 0x4 | 0x8 | 0x10; } /** diff --git a/core/java/android/hardware/biometrics/AuthenticateOptions.java b/core/java/android/hardware/biometrics/AuthenticateOptions.java index 77660713275f..4dc6ea19a35d 100644 --- a/core/java/android/hardware/biometrics/AuthenticateOptions.java +++ b/core/java/android/hardware/biometrics/AuthenticateOptions.java @@ -74,4 +74,7 @@ public interface AuthenticateOptions { /** The attribution tag, if any. */ @Nullable String getAttributionTag(); + + /** If the authentication is requested due to mandatory biometrics being active. */ + boolean isMandatoryBiometrics(); } diff --git a/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl b/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl index 17cd18cc4182..b195225420b9 100644 --- a/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl +++ b/core/java/android/hardware/biometrics/IBiometricAuthenticator.aidl @@ -49,7 +49,7 @@ interface IBiometricAuthenticator { void prepareForAuthentication(boolean requireConfirmation, IBinder token, long operationId, int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId, int cookie, boolean allowBackgroundAuthentication, - boolean isForLegacyFingerprintManager); + boolean isForLegacyFingerprintManager, boolean isMandatoryBiometrics); // Starts authentication with the previously prepared client. void startPreparedClient(int cookie); diff --git a/core/java/android/hardware/face/FaceAuthenticateOptions.java b/core/java/android/hardware/face/FaceAuthenticateOptions.java index 518f902acdcb..8babbfa7169d 100644 --- a/core/java/android/hardware/face/FaceAuthenticateOptions.java +++ b/core/java/android/hardware/face/FaceAuthenticateOptions.java @@ -120,6 +120,8 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable } + /** If the authentication is requested due to mandatory biometrics being active. */ + private boolean mIsMandatoryBiometrics; // Code below generated by codegen v1.0.23. // @@ -188,7 +190,8 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable @AuthenticateReason int authenticateReason, @PowerManager.WakeReason int wakeReason, @NonNull String opPackageName, - @Nullable String attributionTag) { + @Nullable String attributionTag, + boolean isMandatoryBiometrics) { this.mUserId = userId; this.mSensorId = sensorId; this.mDisplayState = displayState; @@ -229,6 +232,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mOpPackageName); this.mAttributionTag = attributionTag; + this.mIsMandatoryBiometrics = isMandatoryBiometrics; // onConstructed(); // You can define this method to get a callback } @@ -261,7 +265,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable * The reason for this operation when requested by the system (sysui), * otherwise AUTHENTICATE_REASON_UNKNOWN. * - * See packages/SystemUI/src/com/android/systemui/deviceentry/shared/FaceAuthReason.kt + * See frameworks/base/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt * for more details about each reason. */ @DataClass.Generated.Member @@ -299,6 +303,14 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable } /** + * If the authentication is requested due to mandatory biometrics being active. + */ + @DataClass.Generated.Member + public boolean isMandatoryBiometrics() { + return mIsMandatoryBiometrics; + } + + /** * The sensor id for this operation. */ @DataClass.Generated.Member @@ -332,6 +344,15 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable return this; } + /** + * If the authentication is requested due to mandatory biometrics being active. + */ + @DataClass.Generated.Member + public @NonNull FaceAuthenticateOptions setIsMandatoryBiometrics( boolean value) { + mIsMandatoryBiometrics = value; + return this; + } + @Override @DataClass.Generated.Member public boolean equals(@Nullable Object o) { @@ -351,7 +372,8 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable && mAuthenticateReason == that.mAuthenticateReason && mWakeReason == that.mWakeReason && java.util.Objects.equals(mOpPackageName, that.mOpPackageName) - && java.util.Objects.equals(mAttributionTag, that.mAttributionTag); + && java.util.Objects.equals(mAttributionTag, that.mAttributionTag) + && mIsMandatoryBiometrics == that.mIsMandatoryBiometrics; } @Override @@ -368,6 +390,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable _hash = 31 * _hash + mWakeReason; _hash = 31 * _hash + java.util.Objects.hashCode(mOpPackageName); _hash = 31 * _hash + java.util.Objects.hashCode(mAttributionTag); + _hash = 31 * _hash + Boolean.hashCode(mIsMandatoryBiometrics); return _hash; } @@ -377,9 +400,10 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable // You can override field parcelling by defining methods like: // void parcelFieldName(Parcel dest, int flags) { ... } - byte flg = 0; + int flg = 0; + if (mIsMandatoryBiometrics) flg |= 0x80; if (mAttributionTag != null) flg |= 0x40; - dest.writeByte(flg); + dest.writeInt(flg); dest.writeInt(mUserId); dest.writeInt(mSensorId); dest.writeInt(mDisplayState); @@ -400,7 +424,8 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable // You can override field unparcelling by defining methods like: // static FieldType unparcelFieldName(Parcel in) { ... } - byte flg = in.readByte(); + int flg = in.readInt(); + boolean isMandatoryBiometrics = (flg & 0x80) != 0; int userId = in.readInt(); int sensorId = in.readInt(); int displayState = in.readInt(); @@ -449,6 +474,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mOpPackageName); this.mAttributionTag = attributionTag; + this.mIsMandatoryBiometrics = isMandatoryBiometrics; // onConstructed(); // You can define this method to get a callback } @@ -481,6 +507,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable private @PowerManager.WakeReason int mWakeReason; private @NonNull String mOpPackageName; private @Nullable String mAttributionTag; + private boolean mIsMandatoryBiometrics; private long mBuilderFieldsSet = 0L; @@ -524,7 +551,7 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable * The reason for this operation when requested by the system (sysui), * otherwise AUTHENTICATE_REASON_UNKNOWN. * - * See packages/SystemUI/src/com/android/systemui/deviceentry/shared/FaceAuthReason.kt + * See frameworks/base/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt * for more details about each reason. */ @DataClass.Generated.Member @@ -573,10 +600,21 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable return this; } + /** + * If the authentication is requested due to mandatory biometrics being active. + */ + @DataClass.Generated.Member + public @NonNull Builder setIsMandatoryBiometrics(boolean value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x80; + mIsMandatoryBiometrics = value; + return this; + } + /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull FaceAuthenticateOptions build() { checkNotUsed(); - mBuilderFieldsSet |= 0x80; // Mark builder used + mBuilderFieldsSet |= 0x100; // Mark builder used if ((mBuilderFieldsSet & 0x1) == 0) { mUserId = defaultUserId(); @@ -606,12 +644,13 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable mAuthenticateReason, mWakeReason, mOpPackageName, - mAttributionTag); + mAttributionTag, + mIsMandatoryBiometrics); return o; } private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x80) != 0) { + if ((mBuilderFieldsSet & 0x100) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } @@ -619,10 +658,10 @@ public class FaceAuthenticateOptions implements AuthenticateOptions, Parcelable } @DataClass.Generated( - time = 1677119626034L, + time = 1723436679828L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/hardware/face/FaceAuthenticateOptions.java", - inputSignatures = "private final int mUserId\nprivate int mSensorId\nprivate final @android.hardware.biometrics.AuthenticateOptions.DisplayState int mDisplayState\npublic static final int AUTHENTICATE_REASON_UNKNOWN\npublic static final int AUTHENTICATE_REASON_STARTED_WAKING_UP\npublic static final int AUTHENTICATE_REASON_PRIMARY_BOUNCER_SHOWN\npublic static final int AUTHENTICATE_REASON_ASSISTANT_VISIBLE\npublic static final int AUTHENTICATE_REASON_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN\npublic static final int AUTHENTICATE_REASON_NOTIFICATION_PANEL_CLICKED\npublic static final int AUTHENTICATE_REASON_OCCLUDING_APP_REQUESTED\npublic static final int AUTHENTICATE_REASON_PICK_UP_GESTURE_TRIGGERED\npublic static final int AUTHENTICATE_REASON_QS_EXPANDED\npublic static final int AUTHENTICATE_REASON_SWIPE_UP_ON_BOUNCER\npublic static final int AUTHENTICATE_REASON_UDFPS_POINTER_DOWN\nprivate final @android.hardware.face.FaceAuthenticateOptions.AuthenticateReason int mAuthenticateReason\nprivate final @android.os.PowerManager.WakeReason int mWakeReason\nprivate @android.annotation.NonNull java.lang.String mOpPackageName\nprivate @android.annotation.Nullable java.lang.String mAttributionTag\nprivate static int defaultUserId()\nprivate static int defaultSensorId()\nprivate static int defaultDisplayState()\nprivate static int defaultAuthenticateReason()\nprivate static int defaultWakeReason()\nprivate static java.lang.String defaultOpPackageName()\nprivate static java.lang.String defaultAttributionTag()\nclass FaceAuthenticateOptions extends java.lang.Object implements [android.hardware.biometrics.AuthenticateOptions, android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true, genSetters=true, genEqualsHashCode=true)") + inputSignatures = "private final int mUserId\nprivate int mSensorId\nprivate final @android.hardware.biometrics.AuthenticateOptions.DisplayState int mDisplayState\npublic static final int AUTHENTICATE_REASON_UNKNOWN\npublic static final int AUTHENTICATE_REASON_STARTED_WAKING_UP\npublic static final int AUTHENTICATE_REASON_PRIMARY_BOUNCER_SHOWN\npublic static final int AUTHENTICATE_REASON_ASSISTANT_VISIBLE\npublic static final int AUTHENTICATE_REASON_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN\npublic static final int AUTHENTICATE_REASON_NOTIFICATION_PANEL_CLICKED\npublic static final int AUTHENTICATE_REASON_OCCLUDING_APP_REQUESTED\npublic static final int AUTHENTICATE_REASON_PICK_UP_GESTURE_TRIGGERED\npublic static final int AUTHENTICATE_REASON_QS_EXPANDED\npublic static final int AUTHENTICATE_REASON_SWIPE_UP_ON_BOUNCER\npublic static final int AUTHENTICATE_REASON_UDFPS_POINTER_DOWN\nprivate final @android.hardware.face.FaceAuthenticateOptions.AuthenticateReason int mAuthenticateReason\nprivate final @android.os.PowerManager.WakeReason int mWakeReason\nprivate @android.annotation.NonNull java.lang.String mOpPackageName\nprivate @android.annotation.Nullable java.lang.String mAttributionTag\nprivate boolean mIsMandatoryBiometrics\nprivate static int defaultUserId()\nprivate static int defaultSensorId()\nprivate static int defaultDisplayState()\nprivate static int defaultAuthenticateReason()\nprivate static int defaultWakeReason()\nprivate static java.lang.String defaultOpPackageName()\nprivate static java.lang.String defaultAttributionTag()\nclass FaceAuthenticateOptions extends java.lang.Object implements [android.hardware.biometrics.AuthenticateOptions, android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true, genSetters=true, genEqualsHashCode=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/hardware/fingerprint/FingerprintAuthenticateOptions.java b/core/java/android/hardware/fingerprint/FingerprintAuthenticateOptions.java index dc66542f4f37..ddf1e5b9dd17 100644 --- a/core/java/android/hardware/fingerprint/FingerprintAuthenticateOptions.java +++ b/core/java/android/hardware/fingerprint/FingerprintAuthenticateOptions.java @@ -97,6 +97,11 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions return null; } + /** + * If the authentication is requested due to mandatory biometrics being active. + */ + private boolean mIsMandatoryBiometrics; + // Code below generated by codegen v1.0.23. // // DO NOT MODIFY! @@ -118,7 +123,8 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions @AuthenticateOptions.DisplayState int displayState, @NonNull String opPackageName, @Nullable String attributionTag, - @Nullable AuthenticateReason.Vendor vendorReason) { + @Nullable AuthenticateReason.Vendor vendorReason, + boolean isMandatoryBiometrics) { this.mUserId = userId; this.mSensorId = sensorId; this.mIgnoreEnrollmentState = ignoreEnrollmentState; @@ -130,6 +136,7 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions NonNull.class, null, mOpPackageName); this.mAttributionTag = attributionTag; this.mVendorReason = vendorReason; + this.mIsMandatoryBiometrics = isMandatoryBiometrics; // onConstructed(); // You can define this method to get a callback } @@ -199,6 +206,14 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions } /** + * If the authentication is requested due to mandatory biometrics being active. + */ + @DataClass.Generated.Member + public boolean isMandatoryBiometrics() { + return mIsMandatoryBiometrics; + } + + /** * The sensor id for this operation. */ @DataClass.Generated.Member @@ -244,6 +259,15 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions return this; } + /** + * If the authentication is requested due to mandatory biometrics being active. + */ + @DataClass.Generated.Member + public @NonNull FingerprintAuthenticateOptions setIsMandatoryBiometrics( boolean value) { + mIsMandatoryBiometrics = value; + return this; + } + @Override @DataClass.Generated.Member public boolean equals(@Nullable Object o) { @@ -263,7 +287,8 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions && mDisplayState == that.mDisplayState && java.util.Objects.equals(mOpPackageName, that.mOpPackageName) && java.util.Objects.equals(mAttributionTag, that.mAttributionTag) - && java.util.Objects.equals(mVendorReason, that.mVendorReason); + && java.util.Objects.equals(mVendorReason, that.mVendorReason) + && mIsMandatoryBiometrics == that.mIsMandatoryBiometrics; } @Override @@ -280,6 +305,7 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions _hash = 31 * _hash + java.util.Objects.hashCode(mOpPackageName); _hash = 31 * _hash + java.util.Objects.hashCode(mAttributionTag); _hash = 31 * _hash + java.util.Objects.hashCode(mVendorReason); + _hash = 31 * _hash + Boolean.hashCode(mIsMandatoryBiometrics); return _hash; } @@ -289,11 +315,12 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions // You can override field parcelling by defining methods like: // void parcelFieldName(Parcel dest, int flags) { ... } - byte flg = 0; + int flg = 0; if (mIgnoreEnrollmentState) flg |= 0x4; + if (mIsMandatoryBiometrics) flg |= 0x80; if (mAttributionTag != null) flg |= 0x20; if (mVendorReason != null) flg |= 0x40; - dest.writeByte(flg); + dest.writeInt(flg); dest.writeInt(mUserId); dest.writeInt(mSensorId); dest.writeInt(mDisplayState); @@ -313,8 +340,9 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions // You can override field unparcelling by defining methods like: // static FieldType unparcelFieldName(Parcel in) { ... } - byte flg = in.readByte(); + int flg = in.readInt(); boolean ignoreEnrollmentState = (flg & 0x4) != 0; + boolean isMandatoryBiometrics = (flg & 0x80) != 0; int userId = in.readInt(); int sensorId = in.readInt(); int displayState = in.readInt(); @@ -333,6 +361,7 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions NonNull.class, null, mOpPackageName); this.mAttributionTag = attributionTag; this.mVendorReason = vendorReason; + this.mIsMandatoryBiometrics = isMandatoryBiometrics; // onConstructed(); // You can define this method to get a callback } @@ -365,6 +394,7 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions private @NonNull String mOpPackageName; private @Nullable String mAttributionTag; private @Nullable AuthenticateReason.Vendor mVendorReason; + private boolean mIsMandatoryBiometrics; private long mBuilderFieldsSet = 0L; @@ -456,10 +486,21 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions return this; } + /** + * If the authentication is requested due to mandatory biometrics being active. + */ + @DataClass.Generated.Member + public @NonNull Builder setIsMandatoryBiometrics(boolean value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x80; + mIsMandatoryBiometrics = value; + return this; + } + /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull FingerprintAuthenticateOptions build() { checkNotUsed(); - mBuilderFieldsSet |= 0x80; // Mark builder used + mBuilderFieldsSet |= 0x100; // Mark builder used if ((mBuilderFieldsSet & 0x1) == 0) { mUserId = defaultUserId(); @@ -489,12 +530,13 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions mDisplayState, mOpPackageName, mAttributionTag, - mVendorReason); + mVendorReason, + mIsMandatoryBiometrics); return o; } private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x80) != 0) { + if ((mBuilderFieldsSet & 0x100) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } @@ -502,10 +544,10 @@ public final class FingerprintAuthenticateOptions implements AuthenticateOptions } @DataClass.Generated( - time = 1689703591032L, + time = 1723436831455L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/hardware/fingerprint/FingerprintAuthenticateOptions.java", - inputSignatures = "private final int mUserId\nprivate int mSensorId\nprivate final boolean mIgnoreEnrollmentState\nprivate final @android.hardware.biometrics.AuthenticateOptions.DisplayState int mDisplayState\nprivate @android.annotation.NonNull java.lang.String mOpPackageName\nprivate @android.annotation.Nullable java.lang.String mAttributionTag\nprivate @android.annotation.Nullable android.hardware.biometrics.common.AuthenticateReason.Vendor mVendorReason\nprivate static int defaultUserId()\nprivate static int defaultSensorId()\nprivate static boolean defaultIgnoreEnrollmentState()\nprivate static int defaultDisplayState()\nprivate static java.lang.String defaultOpPackageName()\nprivate static java.lang.String defaultAttributionTag()\nprivate static android.hardware.biometrics.common.AuthenticateReason.Vendor defaultVendorReason()\nclass FingerprintAuthenticateOptions extends java.lang.Object implements [android.hardware.biometrics.AuthenticateOptions, android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true, genSetters=true, genEqualsHashCode=true)") + inputSignatures = "private final int mUserId\nprivate int mSensorId\nprivate final boolean mIgnoreEnrollmentState\nprivate final @android.hardware.biometrics.AuthenticateOptions.DisplayState int mDisplayState\nprivate @android.annotation.NonNull java.lang.String mOpPackageName\nprivate @android.annotation.Nullable java.lang.String mAttributionTag\nprivate @android.annotation.Nullable android.hardware.biometrics.common.AuthenticateReason.Vendor mVendorReason\nprivate boolean mIsMandatoryBiometrics\nprivate static int defaultUserId()\nprivate static int defaultSensorId()\nprivate static boolean defaultIgnoreEnrollmentState()\nprivate static int defaultDisplayState()\nprivate static java.lang.String defaultOpPackageName()\nprivate static java.lang.String defaultAttributionTag()\nprivate static android.hardware.biometrics.common.AuthenticateReason.Vendor defaultVendorReason()\nclass FingerprintAuthenticateOptions extends java.lang.Object implements [android.hardware.biometrics.AuthenticateOptions, android.os.Parcelable]\n@com.android.internal.util.DataClass(genParcelable=true, genAidl=true, genBuilder=true, genSetters=true, genEqualsHashCode=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java index 23bd30a21c4c..0ed1ab6c8d72 100644 --- a/core/java/android/os/SystemClock.java +++ b/core/java/android/os/SystemClock.java @@ -314,6 +314,15 @@ public final class SystemClock { } /** + * @see #currentNetworkTimeMillis(ITimeDetectorService) + * @hide + */ + public static long currentNetworkTimeMillis() { + return currentNetworkTimeMillis(ITimeDetectorService.Stub + .asInterface(ServiceManager.getService(Context.TIME_DETECTOR_SERVICE))); + } + + /** * Returns milliseconds since January 1, 1970 00:00:00.0 UTC, synchronized * using a remote network source outside the device. * <p> @@ -331,14 +340,14 @@ public final class SystemClock { * at any time. Due to network delays, variations between servers, or local * (client side) clock drift, the accuracy of the returned times cannot be * guaranteed. In extreme cases, consecutive calls to {@link - * #currentNetworkTimeMillis()} could return times that are out of order. + * #currentNetworkTimeMillis(ITimeDetectorService)} could return times that + * are out of order. * * @throws DateTimeException when no network time can be provided. * @hide */ - public static long currentNetworkTimeMillis() { - ITimeDetectorService timeDetectorService = ITimeDetectorService.Stub - .asInterface(ServiceManager.getService(Context.TIME_DETECTOR_SERVICE)); + public static long currentNetworkTimeMillis( + ITimeDetectorService timeDetectorService) { if (timeDetectorService != null) { UnixEpochTime time; try { @@ -380,16 +389,21 @@ public final class SystemClock { * at any time. Due to network delays, variations between servers, or local * (client side) clock drift, the accuracy of the returned times cannot be * guaranteed. In extreme cases, consecutive calls to {@link - * Clock#millis()} on the returned {@link Clock}could return times that are + * Clock#millis()} on the returned {@link Clock} could return times that are * out of order. * * @throws DateTimeException when no network time can be provided. */ public static @NonNull Clock currentNetworkTimeClock() { return new SimpleClock(ZoneOffset.UTC) { + private ITimeDetectorService mSvc; @Override public long millis() { - return SystemClock.currentNetworkTimeMillis(); + if (mSvc == null) { + mSvc = ITimeDetectorService.Stub + .asInterface(ServiceManager.getService(Context.TIME_DETECTOR_SERVICE)); + } + return SystemClock.currentNetworkTimeMillis(mSvc); } }; } diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 536eca6f9a0f..f1ec0e4e9bdc 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -1978,7 +1978,6 @@ public class UserManager { * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) * @see #getUserRestrictions() */ - @FlaggedApi(android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED) public static final String DISALLOW_SIM_GLOBALLY = "no_sim_globally"; @@ -1999,7 +1998,6 @@ public class UserManager { * @see DevicePolicyManager#clearUserRestriction(ComponentName, String) * @see #getUserRestrictions() */ - @FlaggedApi(android.app.admin.flags.Flags.FLAG_ASSIST_CONTENT_USER_RESTRICTION_ENABLED) public static final String DISALLOW_ASSIST_CONTENT = "no_assist_content"; /** diff --git a/core/java/android/permission/TEST_MAPPING b/core/java/android/permission/TEST_MAPPING index a15d9bc1b485..b317b80d5677 100644 --- a/core/java/android/permission/TEST_MAPPING +++ b/core/java/android/permission/TEST_MAPPING @@ -1,15 +1,7 @@ { "presubmit": [ { - "name": "CtsPermissionTestCases", - "options": [ - { - "include-filter": "android.permission.cts.PermissionControllerTest" - }, - { - "include-filter": "android.permission.cts.RuntimePermissionPresentationInfoTest" - } - ] + "name": "CtsPermissionTestCases_Platform" } ], "postsubmit": [ @@ -36,4 +28,4 @@ ] } ] -}
\ No newline at end of file +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 85d23259ffbb..24f52d0151bb 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -19666,6 +19666,8 @@ public final class Settings { public static final int PAIRED_DEVICE_OS_TYPE_ANDROID = 1; /** @hide */ public static final int PAIRED_DEVICE_OS_TYPE_IOS = 2; + /** @hide */ + public static final int PAIRED_DEVICE_OS_TYPE_NONE = 3; /** * The bluetooth settings selected BLE role for the companion. diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java index 013ec5f35761..711c41498929 100644 --- a/core/java/android/service/dreams/DreamOverlayService.java +++ b/core/java/android/service/dreams/DreamOverlayService.java @@ -51,11 +51,14 @@ public abstract class DreamOverlayService extends Service { */ private Executor mExecutor; + private Boolean mCurrentRedirectToWake; + // An {@link IDreamOverlayClient} implementation that identifies itself when forwarding // requests to the {@link DreamOverlayService} private static class OverlayClient extends IDreamOverlayClient.Stub { private final WeakReference<DreamOverlayService> mService; private boolean mShowComplications; + private boolean mIsPreview; private ComponentName mDreamComponent; IDreamOverlayCallback mDreamOverlayCallback; @@ -73,9 +76,11 @@ public abstract class DreamOverlayService extends Service { @Override public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback, - String dreamComponent, boolean shouldShowComplications) throws RemoteException { + String dreamComponent, boolean isPreview, boolean shouldShowComplications) + throws RemoteException { mDreamComponent = ComponentName.unflattenFromString(dreamComponent); mShowComplications = shouldShowComplications; + mIsPreview = isPreview; mDreamOverlayCallback = callback; applyToDream(dreamOverlayService -> dreamOverlayService.startDream(this, params)); } @@ -122,6 +127,10 @@ public abstract class DreamOverlayService extends Service { return mShowComplications; } + private boolean isDreamInPreviewMode() { + return mIsPreview; + } + private ComponentName getComponent() { return mDreamComponent; } @@ -132,6 +141,10 @@ public abstract class DreamOverlayService extends Service { mExecutor.execute(() -> { endDreamInternal(mCurrentClient); mCurrentClient = client; + if (Flags.dreamWakeRedirect() && mCurrentRedirectToWake != null) { + mCurrentClient.redirectWake(mCurrentRedirectToWake); + } + onStartDream(params); }); } @@ -282,8 +295,10 @@ public abstract class DreamOverlayService extends Service { return; } + mCurrentRedirectToWake = redirect; + if (mCurrentClient == null) { - throw new IllegalStateException("redirected wake with no dream present"); + return; } mCurrentClient.redirectWake(redirect); @@ -295,7 +310,6 @@ public abstract class DreamOverlayService extends Service { * * @hide */ - @FlaggedApi(Flags.FLAG_DREAM_WAKE_REDIRECT) public void onWakeRequested() { } @@ -312,6 +326,19 @@ public abstract class DreamOverlayService extends Service { } /** + * Returns whether dream is in preview mode. + */ + @FlaggedApi(Flags.FLAG_PUBLISH_PREVIEW_STATE_TO_OVERLAY) + public final boolean isDreamInPreviewMode() { + if (mCurrentClient == null) { + throw new IllegalStateException( + "requested if preview when no dream active"); + } + + return mCurrentClient.isDreamInPreviewMode(); + } + + /** * Returns the active dream component. * @hide */ diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index c3585e3c5288..ce31e1ea7e38 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1701,6 +1701,7 @@ public class DreamService extends Service implements Window.Callback { try { overlay.startDream(mWindow.getAttributes(), mOverlayCallback, mDreamComponent.flattenToString(), + mPreviewMode, mShouldShowComplications); } catch (RemoteException e) { Log.e(mTag, "could not send window attributes:" + e); diff --git a/core/java/android/service/dreams/IDreamOverlayClient.aidl b/core/java/android/service/dreams/IDreamOverlayClient.aidl index 0eb15a07edf7..e9df402c6d07 100644 --- a/core/java/android/service/dreams/IDreamOverlayClient.aidl +++ b/core/java/android/service/dreams/IDreamOverlayClient.aidl @@ -31,11 +31,12 @@ interface IDreamOverlayClient { * @param callback The {@link IDreamOverlayCallback} for requesting actions such as exiting the * dream. * @param dreamComponent The component name of the dream service requesting overlay. + * @param isPreview Whether the dream is in preview mode. * @param shouldShowComplications Whether the dream overlay should show complications, e.g. clock * and weather. */ void startDream(in LayoutParams params, in IDreamOverlayCallback callback, - in String dreamComponent, in boolean shouldShowComplications); + in String dreamComponent, in boolean isPreview, in boolean shouldShowComplications); /** Called when the dream is waking, to do any exit animations */ void wakeUp(); diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index 83e0adfca3be..72f2de805474 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -57,3 +57,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "publish_preview_state_to_overlay" + namespace: "systemui" + description: "send preview information from dream to overlay" + bug: "333734282" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index b1df51f7affa..7c8cd932f737 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -488,7 +488,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @Override public Interpolator getInsetsInterpolator(boolean hasZeroInsetsIme) { if ((mRequestedTypes & ime()) != 0) { - if (mHasAnimationCallbacks && hasZeroInsetsIme) { + if (mHasAnimationCallbacks && !hasZeroInsetsIme) { return SYNC_IME_INTERPOLATOR; } else if (mShow) { return LINEAR_OUT_SLOW_IN_INTERPOLATOR; @@ -536,7 +536,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @Override public long getDurationMs(boolean hasZeroInsetsIme) { if ((mRequestedTypes & ime()) != 0) { - if (mHasAnimationCallbacks && hasZeroInsetsIme) { + if (mHasAnimationCallbacks && !hasZeroInsetsIme) { return ANIMATION_DURATION_SYNC_IME_MS; } else { return ANIMATION_DURATION_UNSYNC_IME_MS; diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 1083f64513b1..ec79f94a6dd3 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -1141,6 +1141,7 @@ public final class TransitionInfo implements Parcelable { // Customize activity transition animation private CustomActivityTransition mCustomActivityOpenTransition; private CustomActivityTransition mCustomActivityCloseTransition; + private int mUserId; private AnimationOptions(int type) { mType = type; @@ -1159,6 +1160,7 @@ public final class TransitionInfo implements Parcelable { mAnimations = in.readInt(); mCustomActivityOpenTransition = in.readTypedObject(CustomActivityTransition.CREATOR); mCustomActivityCloseTransition = in.readTypedObject(CustomActivityTransition.CREATOR); + mUserId = in.readInt(); } /** Make basic customized animation for a package */ @@ -1283,6 +1285,14 @@ public final class TransitionInfo implements Parcelable { return options; } + public void setUserId(int userId) { + mUserId = userId; + } + + public int getUserId() { + return mUserId; + } + public int getType() { return mType; } @@ -1349,6 +1359,7 @@ public final class TransitionInfo implements Parcelable { dest.writeInt(mAnimations); dest.writeTypedObject(mCustomActivityOpenTransition, flags); dest.writeTypedObject(mCustomActivityCloseTransition, flags); + dest.writeInt(mUserId); } @NonNull @@ -1406,6 +1417,7 @@ public final class TransitionInfo implements Parcelable { if (mExitResId != DEFAULT_ANIMATION_RESOURCES_ID) { sb.append(" exitResId=").append(mExitResId); } + sb.append(" mUserId=").append(mUserId); sb.append('}'); return sb.toString(); } diff --git a/core/java/android/window/flags/wallpaper_manager.aconfig b/core/java/android/window/flags/wallpaper_manager.aconfig index 8c6721a7e96a..efacc346ac0a 100644 --- a/core/java/android/window/flags/wallpaper_manager.aconfig +++ b/core/java/android/window/flags/wallpaper_manager.aconfig @@ -49,3 +49,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "avoid_rebinding_intentionally_disconnected_wallpaper" + namespace: "systemui" + description: "Prevents rebinding with intentionally disconnected wallpaper services." + bug: "332871851" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 67fc27042ff8..a786fc24d9a7 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -246,3 +246,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "always_capture_activity_snapshot" + namespace: "windowing_frontend" + description: "Always capture activity snapshot regardless predictive back status" + bug: "362183912" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/core/java/com/android/internal/infra/TEST_MAPPING b/core/java/com/android/internal/infra/TEST_MAPPING index e4550c0db135..35f0553d41d7 100644 --- a/core/java/com/android/internal/infra/TEST_MAPPING +++ b/core/java/com/android/internal/infra/TEST_MAPPING @@ -9,15 +9,7 @@ ] }, { - "name": "CtsPermissionTestCases", - "options": [ - { - "include-filter": "android.permission.cts.PermissionControllerTest" - }, - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - } - ] + "name": "CtsPermissionTestCases_Platform" }, { "name": "FrameworksCoreTests_internal_infra" diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java index 238e6f56153b..201f26760956 100644 --- a/core/java/com/android/internal/policy/TransitionAnimation.java +++ b/core/java/com/android/internal/policy/TransitionAnimation.java @@ -49,7 +49,7 @@ import android.hardware.HardwareBuffer; import android.media.Image; import android.media.ImageReader; import android.os.Handler; -import android.os.SystemProperties; +import android.os.UserHandle; import android.util.Slog; import android.view.InflateException; import android.view.SurfaceControl; @@ -187,23 +187,44 @@ public class TransitionAnimation { return createHiddenByKeyguardExit(mContext, mInterpolator, onWallpaper, toShade, subtle); } + /** Load keyguard unocclude animation for user. */ + @Nullable + public Animation loadKeyguardUnoccludeAnimation(int userId) { + return loadDefaultAnimationRes(com.android.internal.R.anim.wallpaper_open_exit, userId); + } + + /** Same as {@code loadKeyguardUnoccludeAnimation} for current user. */ @Nullable public Animation loadKeyguardUnoccludeAnimation() { - return loadDefaultAnimationRes(com.android.internal.R.anim.wallpaper_open_exit); + return loadKeyguardUnoccludeAnimation(UserHandle.USER_CURRENT); } + /** Load voice activity open animation for user. */ @Nullable - public Animation loadVoiceActivityOpenAnimation(boolean enter) { + public Animation loadVoiceActivityOpenAnimation(boolean enter, int userId) { return loadDefaultAnimationRes(enter ? com.android.internal.R.anim.voice_activity_open_enter - : com.android.internal.R.anim.voice_activity_open_exit); + : com.android.internal.R.anim.voice_activity_open_exit, userId); } + /** Same as {@code loadVoiceActivityOpenAnimation} for current user. */ @Nullable - public Animation loadVoiceActivityExitAnimation(boolean enter) { + public Animation loadVoiceActivityOpenAnimation(boolean enter) { + return loadVoiceActivityOpenAnimation(enter, UserHandle.USER_CURRENT); + } + + /** Load voice activity exit animation for user. */ + @Nullable + public Animation loadVoiceActivityExitAnimation(boolean enter, int userId) { return loadDefaultAnimationRes(enter ? com.android.internal.R.anim.voice_activity_close_enter - : com.android.internal.R.anim.voice_activity_close_exit); + : com.android.internal.R.anim.voice_activity_close_exit, userId); + } + + /** Same as {@code loadVoiceActivityExitAnimation} for current user. */ + @Nullable + public Animation loadVoiceActivityExitAnimation(boolean enter) { + return loadVoiceActivityExitAnimation(enter, UserHandle.USER_CURRENT); } @Nullable @@ -211,10 +232,17 @@ public class TransitionAnimation { return loadAnimationRes(packageName, resId); } + /** Load cross profile app enter animation for user. */ @Nullable - public Animation loadCrossProfileAppEnterAnimation() { + public Animation loadCrossProfileAppEnterAnimation(int userId) { return loadAnimationRes(DEFAULT_PACKAGE, - com.android.internal.R.anim.task_open_enter_cross_profile_apps); + com.android.internal.R.anim.task_open_enter_cross_profile_apps, userId); + } + + /** Same as {@code loadCrossProfileAppEnterAnimation} for current user. */ + @Nullable + public Animation loadCrossProfileAppEnterAnimation() { + return loadCrossProfileAppEnterAnimation(UserHandle.USER_CURRENT); } @Nullable @@ -230,11 +258,11 @@ public class TransitionAnimation { appRect.height(), 0, null); } - /** Load animation by resource Id from specific package. */ + /** Load animation by resource Id from specific package for user. */ @Nullable - public Animation loadAnimationRes(String packageName, int resId) { + public Animation loadAnimationRes(String packageName, int resId, int userId) { if (ResourceId.isValid(resId)) { - AttributeCache.Entry ent = getCachedAnimations(packageName, resId); + AttributeCache.Entry ent = getCachedAnimations(packageName, resId, userId); if (ent != null) { return loadAnimationSafely(ent.context, resId, mTag); } @@ -242,10 +270,22 @@ public class TransitionAnimation { return null; } - /** Load animation by resource Id from android package. */ + /** Same as {@code loadAnimationRes} for current user. */ + @Nullable + public Animation loadAnimationRes(String packageName, int resId) { + return loadAnimationRes(packageName, resId, UserHandle.USER_CURRENT); + } + + /** Load animation by resource Id from android package for user. */ + @Nullable + public Animation loadDefaultAnimationRes(int resId, int userId) { + return loadAnimationRes(DEFAULT_PACKAGE, resId, userId); + } + + /** Same as {@code loadDefaultAnimationRes} for current user. */ @Nullable public Animation loadDefaultAnimationRes(int resId) { - return loadAnimationRes(DEFAULT_PACKAGE, resId); + return loadAnimationRes(DEFAULT_PACKAGE, resId, UserHandle.USER_CURRENT); } /** Load animation by attribute Id from specific LayoutParams */ @@ -378,10 +418,10 @@ public class TransitionAnimation { } @Nullable - private AttributeCache.Entry getCachedAnimations(String packageName, int resId) { + private AttributeCache.Entry getCachedAnimations(String packageName, int resId, int userId) { if (mDebug) { - Slog.v(mTag, "Loading animations: package=" - + packageName + " resId=0x" + Integer.toHexString(resId)); + Slog.v(mTag, "Loading animations: package=" + packageName + " resId=0x" + + Integer.toHexString(resId) + " for user=" + userId); } if (packageName != null) { if ((resId & 0xFF000000) == 0x01000000) { @@ -392,11 +432,16 @@ public class TransitionAnimation { + packageName); } return AttributeCache.instance().get(packageName, resId, - com.android.internal.R.styleable.WindowAnimation); + com.android.internal.R.styleable.WindowAnimation, userId); } return null; } + @Nullable + private AttributeCache.Entry getCachedAnimations(String packageName, int resId) { + return getCachedAnimations(packageName, resId, UserHandle.USER_CURRENT); + } + /** Returns window animation style ID from {@link LayoutParams} or from system in some cases */ public int getAnimationStyleResId(@NonNull LayoutParams lp) { int resId = lp.windowAnimations; diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index 5c06b87f78ac..ef6bece0cc0c 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -195,7 +195,7 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, int defaultLogFromLevelInt = configStream.readInt(DEFAULT_LOG_FROM_LEVEL); if (defaultLogFromLevelInt < defaultLogFromLevel.ordinal()) { defaultLogFromLevel = - logLevelFromInt(configStream.readInt(DEFAULT_LOG_FROM_LEVEL)); + logLevelFromInt(defaultLogFromLevelInt); } break; case (int) TRACING_MODE: diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp index 2068bd7bc8ea..46b4695a9cec 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -466,10 +466,25 @@ class JavaBBinderHolder public: sp<JavaBBinder> get(JNIEnv* env, jobject obj) { - AutoMutex _l(mLock); - sp<JavaBBinder> b = mBinder.promote(); - if (b == NULL) { - b = new JavaBBinder(env, obj); + sp<JavaBBinder> b; + { + AutoMutex _l(mLock); + // must take lock to promote because we set the same wp<> + // on another thread. + b = mBinder.promote(); + } + + if (b) return b; + + // b/360067751: constructor may trigger GC, so call outside lock + b = new JavaBBinder(env, obj); + + { + AutoMutex _l(mLock); + // if it was constructed on another thread in the meantime, + // return that. 'b' will just get destructed. + if (sp<JavaBBinder> b2 = mBinder.promote(); b2) return b2; + if (mVintf) { ::android::internal::Stability::markVintf(b.get()); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 91c33702d3e3..17ff2ebc4f20 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -843,6 +843,7 @@ <protected-broadcast android:name="android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED" /> <protected-broadcast android:name="android.intent.action.MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED" /> <protected-broadcast android:name="com.android.uwb.uwbcountrycode.GEOCODE_RETRY" /> + <protected-broadcast android:name="android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED" /> <!-- ====================================================================== --> <!-- RUNTIME PERMISSIONS --> @@ -3765,7 +3766,6 @@ privileged app such as the Assistant app. <p>Protection level: internal|role <p>Intended for use by the DEVICE_POLICY_MANAGEMENT role only. - @FlaggedApi("android.app.admin.flags.assist_content_user_restriction_enabled") --> <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ASSIST_CONTENT" android:protectionLevel="internal|role" /> @@ -4026,7 +4026,6 @@ APIs protected by this permission on users different to the calling user. <p>Protection level: internal|role <p>Intended for use by the DEVICE_POLICY_MANAGEMENT role only. - @FlaggedApi("android.app.admin.flags.esim_management_enabled") --> <permission android:name="android.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS" android:protectionLevel="internal|role" /> diff --git a/core/res/res/drawable/ic_zen_priority_modes.xml b/core/res/res/drawable/ic_zen_priority_modes.xml new file mode 100644 index 000000000000..98de27bb349f --- /dev/null +++ b/core/res/res/drawable/ic_zen_priority_modes.xml @@ -0,0 +1,25 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?android:attr/colorControlNormal"> + <path + android:pathData="M160,480v-80h320v80L160,480ZM480,880q-80,0 -153.5,-29.5T196,764l56,-56q47,44 106,68t122,24q133,0 226.5,-93.5T800,480q0,-133 -93.5,-226.5T480,160v-80q83,0 155.5,31.5t127,86q54.5,54.5 86,127T880,480q0,82 -31.5,155t-86,127.5q-54.5,54.5 -127,86T480,880Z" + android:fillColor="@android:color/white"/> +</vector> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index a7240ffc6012..69437b44fd6e 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -440,10 +440,6 @@ <string name="config_satellite_carrier_roaming_esos_provisioned_class" translatable="false"></string> <java-symbol type="string" name="config_satellite_carrier_roaming_esos_provisioned_class" /> - <!-- Telephony satellite gateway intent for handling carrier roaming to satellite is using ESOS messaging. --> - <string name="config_satellite_carrier_roaming_esos_provisioned_intent_action" translatable="false"></string> - <java-symbol type="string" name="config_satellite_carrier_roaming_esos_provisioned_intent_action" /> - <!-- The time duration in minutes to wait before retry validating a possible change in satellite allowed region. The default value is 10 minutes. --> <integer name="config_satellite_delay_minutes_before_retry_validating_possible_change_in_allowed_region">10</integer> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index e1394bcacccd..dc99634ddabc 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1504,26 +1504,26 @@ please see styles_device_defaults.xml. <!-- @hide --> <style name="PointerIconVectorStyleFillGreen"> - <item name="pointerIconVectorFill">#6DD58C</item> - <item name="pointerIconVectorFillInverse">#6DD58C</item> + <item name="pointerIconVectorFill">#1AA64A</item> + <item name="pointerIconVectorFillInverse">#1AA64A</item> </style> <!-- @hide --> <style name="PointerIconVectorStyleFillYellow"> - <item name="pointerIconVectorFill">#FDD663</item> - <item name="pointerIconVectorFillInverse">#FDD663</item> + <item name="pointerIconVectorFill">#F55E57</item> + <item name="pointerIconVectorFillInverse">#F55E57</item> </style> <!-- @hide --> <style name="PointerIconVectorStyleFillPink"> - <item name="pointerIconVectorFill">#F2B8B5</item> - <item name="pointerIconVectorFillInverse">#F2B8B5</item> + <item name="pointerIconVectorFill">#F94AAB</item> + <item name="pointerIconVectorFillInverse">#F94AAB</item> </style> <!-- @hide --> <style name="PointerIconVectorStyleFillBlue"> - <item name="pointerIconVectorFill">#8AB4F8</item> - <item name="pointerIconVectorFillInverse">#8AB4F8</item> + <item name="pointerIconVectorFill">#009DC9</item> + <item name="pointerIconVectorFillInverse">#009DC9</item> </style> <!-- @hide --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index bbe661e78b1d..74922aca3b02 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5566,6 +5566,7 @@ <java-symbol type="string" name="recs_notification_channel_label"/> <!-- Priority Modes icons --> + <java-symbol type="drawable" name="ic_zen_priority_modes" /> <java-symbol type="drawable" name="ic_zen_mode_type_bedtime" /> <java-symbol type="drawable" name="ic_zen_mode_type_driving" /> <java-symbol type="drawable" name="ic_zen_mode_type_immersive" /> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java index 69a68c85a2fe..0726624a05f8 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java @@ -24,6 +24,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import java.util.ArrayList; import java.util.List; /** @@ -80,9 +81,14 @@ class BackupHelper { } if (DEBUG) Log.d(TAG, "Start to back up " + taskContainers); + final List<ParcelableTaskContainerData> parcelableTaskContainerDataList = new ArrayList<>( + taskContainers.size()); + for (TaskContainer taskContainer : taskContainers) { + parcelableTaskContainerDataList.add(taskContainer.getParcelableData()); + } final Bundle state = new Bundle(); - state.setClassLoader(TaskContainer.class.getClassLoader()); - state.putParcelableList(KEY_TASK_CONTAINERS, taskContainers); + state.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); + state.putParcelableList(KEY_TASK_CONTAINERS, parcelableTaskContainerDataList); mController.setSavedState(state); } @@ -91,10 +97,12 @@ class BackupHelper { return; } - final List<TaskContainer> taskContainers = savedState.getParcelableArrayList( - KEY_TASK_CONTAINERS, TaskContainer.class); - for (TaskContainer taskContainer : taskContainers) { - if (DEBUG) Log.d(TAG, "restore task " + taskContainer.getTaskId()); + final List<ParcelableTaskContainerData> parcelableTaskContainerDataList = + savedState.getParcelableArrayList(KEY_TASK_CONTAINERS, + ParcelableTaskContainerData.class); + for (ParcelableTaskContainerData data : parcelableTaskContainerDataList) { + final TaskContainer taskContainer = new TaskContainer(data, mController); + if (DEBUG) Log.d(TAG, "Restoring task " + taskContainer.getTaskId()); // TODO(b/289875940): implement the TaskContainer restoration. } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java new file mode 100644 index 000000000000..817cfce69b2e --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java @@ -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 androidx.window.extensions.embedding; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * This class holds the Parcelable data of a {@link SplitContainer}. + */ +class ParcelableSplitContainerData implements Parcelable { + + /** + * A reference to the target {@link SplitContainer} that owns the data. This will not be + * parcelled and will be {@code null} when the data is created from a parcel. + */ + @Nullable + final SplitContainer mSplitContainer; + + @NonNull + final IBinder mToken; + + @NonNull + private final IBinder mPrimaryContainerToken; + + @NonNull + private final IBinder mSecondaryContainerToken; + + // TODO(b/289875940): making this as non-null once the tag can be auto-generated from the rule. + @Nullable + final String mSplitRuleTag; + + /** + * Whether the selection of which container is primary can be changed at runtime. Runtime + * updates is currently possible only for {@link SplitPinContainer} + * + * @see SplitPinContainer + */ + final boolean mIsPrimaryContainerMutable; + + ParcelableSplitContainerData(@NonNull SplitContainer splitContainer, @NonNull IBinder token, + @NonNull IBinder primaryContainerToken, @NonNull IBinder secondaryContainerToken, + @Nullable String splitRuleTag, boolean isPrimaryContainerMutable) { + mSplitContainer = splitContainer; + mToken = token; + mPrimaryContainerToken = primaryContainerToken; + mSecondaryContainerToken = secondaryContainerToken; + mSplitRuleTag = splitRuleTag; + mIsPrimaryContainerMutable = isPrimaryContainerMutable; + } + + private ParcelableSplitContainerData(Parcel in) { + mSplitContainer = null; + mToken = in.readStrongBinder(); + mPrimaryContainerToken = in.readStrongBinder(); + mSecondaryContainerToken = in.readStrongBinder(); + mSplitRuleTag = in.readString(); + mIsPrimaryContainerMutable = in.readBoolean(); + } + + public static final Creator<ParcelableSplitContainerData> CREATOR = new Creator<>() { + @Override + public ParcelableSplitContainerData createFromParcel(Parcel in) { + return new ParcelableSplitContainerData(in); + } + + @Override + public ParcelableSplitContainerData[] newArray(int size) { + return new ParcelableSplitContainerData[size]; + } + }; + + @NonNull + private IBinder getPrimaryContainerToken() { + return mSplitContainer != null ? mSplitContainer.getPrimaryContainer().getToken() + : mPrimaryContainerToken; + } + + @NonNull + private IBinder getSecondaryContainerToken() { + return mSplitContainer != null ? mSplitContainer.getSecondaryContainer().getToken() + : mSecondaryContainerToken; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mToken); + dest.writeStrongBinder(getPrimaryContainerToken()); + dest.writeStrongBinder(getSecondaryContainerToken()); + dest.writeString(mSplitRuleTag); + dest.writeBoolean(mIsPrimaryContainerMutable); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java new file mode 100644 index 000000000000..7377d005cda4 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java @@ -0,0 +1,127 @@ +/* + * 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 androidx.window.extensions.embedding; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class holds the Parcelable data of a {@link TaskContainer}. + */ +class ParcelableTaskContainerData implements Parcelable { + + /** + * A reference to the target {@link TaskContainer} that owns the data. This will not be + * parcelled and will be {@code null} when the data is created from a parcel. + */ + @Nullable + final TaskContainer mTaskContainer; + + /** + * The unique task id. + */ + final int mTaskId; + + /** + * The parcelable data of the active TaskFragmentContainers in this Task. + * Note that this will only be populated before parcelling, and will not be copied when + * making a new instance copy. + */ + @NonNull + private final List<ParcelableTaskFragmentContainerData> + mParcelableTaskFragmentContainerDataList = new ArrayList<>(); + + /** + * The parcelable data of the SplitContainers in this Task. + * Note that this will only be populated before parcelling, and will not be copied when + * making a new instance copy. + */ + @NonNull + private final List<ParcelableSplitContainerData> mParcelableSplitContainerDataList = + new ArrayList<>(); + + ParcelableTaskContainerData(int taskId, @NonNull TaskContainer taskContainer) { + if (taskId == INVALID_TASK_ID) { + throw new IllegalArgumentException("Invalid Task id"); + } + + mTaskId = taskId; + mTaskContainer = taskContainer; + } + + ParcelableTaskContainerData(@NonNull ParcelableTaskContainerData data, + @NonNull TaskContainer taskContainer) { + mTaskId = data.mTaskId; + mTaskContainer = taskContainer; + } + + private ParcelableTaskContainerData(Parcel in) { + mTaskId = in.readInt(); + mTaskContainer = null; + in.readParcelableList(mParcelableTaskFragmentContainerDataList, + ParcelableTaskFragmentContainerData.class.getClassLoader(), + ParcelableTaskFragmentContainerData.class); + in.readParcelableList(mParcelableSplitContainerDataList, + ParcelableSplitContainerData.class.getClassLoader(), + ParcelableSplitContainerData.class); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mTaskId); + dest.writeParcelableList(getParcelableTaskFragmentContainerDataList(), flags); + dest.writeParcelableList(getParcelableSplitContainerDataList(), flags); + } + + @NonNull + List<? extends ParcelableTaskFragmentContainerData> + getParcelableTaskFragmentContainerDataList() { + return mTaskContainer != null ? mTaskContainer.getParcelableTaskFragmentContainerDataList() + : mParcelableTaskFragmentContainerDataList; + } + + @NonNull + List<? extends ParcelableSplitContainerData> getParcelableSplitContainerDataList() { + return mTaskContainer != null ? mTaskContainer.getParcelableSplitContainerDataList() + : mParcelableSplitContainerDataList; + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ParcelableTaskContainerData> CREATOR = new Creator<>() { + @Override + public ParcelableTaskContainerData createFromParcel(Parcel in) { + return new ParcelableTaskContainerData(in); + } + + @Override + public ParcelableTaskContainerData[] newArray(int size) { + return new ParcelableTaskContainerData[size]; + } + }; +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskFragmentContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskFragmentContainerData.java new file mode 100644 index 000000000000..a79a89a210ac --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskFragmentContainerData.java @@ -0,0 +1,105 @@ +/* + * 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 androidx.window.extensions.embedding; + +import android.app.Activity; +import android.graphics.Rect; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * This class holds the Parcelable data of a {@link TaskFragmentContainer}. + */ +class ParcelableTaskFragmentContainerData implements Parcelable { + + /** + * Client-created token that uniquely identifies the task fragment container instance. + */ + @NonNull + final IBinder mToken; + + /** + * The tag specified in launch options. {@code null} if this taskFragment container is not an + * overlay container. + */ + @Nullable + final String mOverlayTag; + + /** + * The associated {@link Activity#getActivityToken()} of the overlay container. + * Must be {@code null} for non-overlay container. + * <p> + * If an overlay container is associated with an activity, this overlay container will be + * dismissed when the associated activity is destroyed. If the overlay container is visible, + * activity will be launched on top of the overlay container and expanded to fill the parent + * container. + */ + @Nullable + final IBinder mAssociatedActivityToken; + + /** + * Bounds that were requested last via {@link android.window.WindowContainerTransaction}. + */ + @NonNull + final Rect mLastRequestedBounds; + + ParcelableTaskFragmentContainerData(@NonNull IBinder token, @Nullable String overlayTag, + @Nullable IBinder associatedActivityToken) { + mToken = token; + mOverlayTag = overlayTag; + mAssociatedActivityToken = associatedActivityToken; + mLastRequestedBounds = new Rect(); + } + + private ParcelableTaskFragmentContainerData(Parcel in) { + mToken = in.readStrongBinder(); + mOverlayTag = in.readString(); + mAssociatedActivityToken = in.readStrongBinder(); + mLastRequestedBounds = in.readTypedObject(Rect.CREATOR); + } + + public static final Creator<ParcelableTaskFragmentContainerData> CREATOR = new Creator<>() { + @Override + public ParcelableTaskFragmentContainerData createFromParcel(Parcel in) { + return new ParcelableTaskFragmentContainerData(in); + } + + @Override + public ParcelableTaskFragmentContainerData[] newArray(int size) { + return new ParcelableTaskFragmentContainerData[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mToken); + dest.writeString(mOverlayTag); + dest.writeStrongBinder(mAssociatedActivityToken); + dest.writeTypedObject(mLastRequestedBounds, flags); + } + +} + diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 39cfacec8447..6d436ec01d98 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -33,6 +33,8 @@ import androidx.window.extensions.core.util.function.Function; */ class SplitContainer { @NonNull + private final ParcelableSplitContainerData mParcelableData; + @NonNull private TaskFragmentContainer mPrimaryContainer; @NonNull private final TaskFragmentContainer mSecondaryContainer; @@ -44,16 +46,6 @@ class SplitContainer { /** @see SplitContainer#getDefaultSplitAttributes() */ @NonNull private SplitAttributes mDefaultSplitAttributes; - @NonNull - private final IBinder mToken; - - /** - * Whether the selection of which container is primary can be changed at runtime. Runtime - * updates is currently possible only for {@link SplitPinContainer} - * - * @see SplitPinContainer - */ - private final boolean mIsPrimaryContainerMutable; SplitContainer(@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @@ -69,13 +61,14 @@ class SplitContainer { @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes, boolean isPrimaryContainerMutable) { + mParcelableData = new ParcelableSplitContainerData(this, new Binder("SplitContainer"), + primaryContainer.getToken(), secondaryContainer.getToken(), splitRule.getTag(), + isPrimaryContainerMutable); mPrimaryContainer = primaryContainer; mSecondaryContainer = secondaryContainer; mSplitRule = splitRule; mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes(); mCurrentSplitAttributes = splitAttributes; - mToken = new Binder("SplitContainer"); - mIsPrimaryContainerMutable = isPrimaryContainerMutable; if (shouldFinishPrimaryWithSecondary(splitRule)) { if (mPrimaryContainer.getRunningActivityCount() == 1 @@ -94,7 +87,7 @@ class SplitContainer { } void setPrimaryContainer(@NonNull TaskFragmentContainer primaryContainer) { - if (!mIsPrimaryContainerMutable) { + if (!mParcelableData.mIsPrimaryContainerMutable) { throw new IllegalStateException("Cannot update primary TaskFragmentContainer"); } mPrimaryContainer = primaryContainer; @@ -150,7 +143,12 @@ class SplitContainer { @NonNull IBinder getToken() { - return mToken; + return mParcelableData.mToken; + } + + @NonNull + ParcelableSplitContainerData getParcelableData() { + return mParcelableData; } /** @@ -201,7 +199,7 @@ class SplitContainer { return null; } return new SplitInfo(primaryActivityStack, secondaryActivityStack, - mCurrentSplitAttributes, SplitInfo.Token.createFromBinder(mToken)); + mCurrentSplitAttributes, SplitInfo.Token.createFromBinder(mParcelableData.mToken)); } static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index fb8efc4ad490..563732027a6c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -169,7 +169,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mController = controller; final Bundle outSavedState = new Bundle(); if (Flags.aeBackStackRestore()) { - outSavedState.setClassLoader(TaskContainer.class.getClassLoader()); + outSavedState.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); registerOrganizer(false /* isSystemOrganizer */, outSavedState); } else { registerOrganizer(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 5795e8da18c2..82dfda58fc75 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -16,7 +16,6 @@ package androidx.window.extensions.embedding; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -32,8 +31,6 @@ import android.app.WindowConfiguration.WindowingMode; import android.content.res.Configuration; import android.graphics.Rect; import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.Log; @@ -57,11 +54,12 @@ import java.util.List; import java.util.Set; /** Represents TaskFragments and split pairs below a Task. */ -class TaskContainer implements Parcelable { +class TaskContainer { private static final String TAG = TaskContainer.class.getSimpleName(); - /** The unique task id. */ - private final int mTaskId; + /** Parcelable data of this TaskContainer. */ + @NonNull + private final ParcelableTaskContainerData mParcelableTaskContainerData; /** Active TaskFragments in this Task. */ @NonNull @@ -130,11 +128,9 @@ class TaskContainer implements Parcelable { * @param splitController The {@link SplitController}. */ TaskContainer(int taskId, @NonNull Activity activityInTask, - @Nullable SplitController splitController) { - if (taskId == INVALID_TASK_ID) { - throw new IllegalArgumentException("Invalid Task id"); - } - mTaskId = taskId; + @NonNull SplitController splitController) { + mParcelableTaskContainerData = new ParcelableTaskContainerData(taskId, this); + final TaskProperties taskProperties = TaskProperties .getTaskPropertiesFromActivity(activityInTask); mInfo = new TaskFragmentParentInfo( @@ -148,8 +144,44 @@ class TaskContainer implements Parcelable { mSplitController = splitController; } + /** This is only used when restoring it from a {@link ParcelableTaskContainerData}. */ + TaskContainer(@NonNull ParcelableTaskContainerData data, + @NonNull SplitController splitController) { + mParcelableTaskContainerData = new ParcelableTaskContainerData(data, this); + mSplitController = splitController; + for (ParcelableTaskFragmentContainerData tfData : + data.getParcelableTaskFragmentContainerDataList()) { + final TaskFragmentContainer container = + new TaskFragmentContainer(tfData, splitController, this); + mContainers.add(container); + } + } + + @NonNull + ParcelableTaskContainerData getParcelableData() { + return mParcelableTaskContainerData; + } + + @NonNull + List<ParcelableTaskFragmentContainerData> getParcelableTaskFragmentContainerDataList() { + final List<ParcelableTaskFragmentContainerData> data = new ArrayList<>(mContainers.size()); + for (TaskFragmentContainer container : mContainers) { + data.add(container.getParcelableData()); + } + return data; + } + + @NonNull + List<ParcelableSplitContainerData> getParcelableSplitContainerDataList() { + final List<ParcelableSplitContainerData> data = new ArrayList<>(mSplitContainers.size()); + for (SplitContainer splitContainer : mSplitContainers) { + data.add(splitContainer.getParcelableData()); + } + return data; + } + int getTaskId() { - return mTaskId; + return mParcelableTaskContainerData.mTaskId; } int getDisplayId() { @@ -680,34 +712,6 @@ class TaskContainer implements Parcelable { return activityStacks; } - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mTaskId); - // TODO(b/289875940) - } - - protected TaskContainer(Parcel in) { - mTaskId = in.readInt(); - // TODO(b/289875940) - } - - public static final Creator<TaskContainer> CREATOR = new Creator<>() { - @Override - public TaskContainer createFromParcel(Parcel in) { - return new TaskContainer(in); - } - - @Override - public TaskContainer[] newArray(int size) { - return new TaskContainer[size]; - } - }; - /** A wrapper class which contains the information of {@link TaskContainer} */ static final class TaskProperties { private final int mDisplayId; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index dc6506b070af..dc1d983997c6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -53,14 +53,12 @@ import java.util.Objects; class TaskFragmentContainer { private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; + /** Parcelable data of this TaskFragmentContainer. */ @NonNull - private final SplitController mController; + private final ParcelableTaskFragmentContainerData mParcelableData; - /** - * Client-created token that uniquely identifies the task fragment container instance. - */ @NonNull - private final IBinder mToken; + private final SplitController mController; /** Parent leaf Task. */ @NonNull @@ -103,9 +101,6 @@ class TaskFragmentContainer { */ private final List<IBinder> mActivitiesToFinishOnExit = new ArrayList<>(); - @Nullable - private final String mOverlayTag; - /** * The launch options that was used to create this container. Must not {@link Bundle#isEmpty()} * for {@link #isOverlay()} container. @@ -113,29 +108,13 @@ class TaskFragmentContainer { @NonNull private final Bundle mLaunchOptions = new Bundle(); - /** - * The associated {@link Activity#getActivityToken()} of the overlay container. - * Must be {@code null} for non-overlay container. - * <p> - * If an overlay container is associated with an activity, this overlay container will be - * dismissed when the associated activity is destroyed. If the overlay container is visible, - * activity will be launched on top of the overlay container and expanded to fill the parent - * container. - */ - @Nullable - private final IBinder mAssociatedActivityToken; - /** Indicates whether the container was cleaned up after the last activity was removed. */ private boolean mIsFinished; /** - * Bounds that were requested last via {@link android.window.WindowContainerTransaction}. - */ - private final Rect mLastRequestedBounds = new Rect(); - - /** * Windowing mode that was requested last via {@link android.window.WindowContainerTransaction}. */ + // TODO(b/289875940): review this and other field that might need to be moved in the base class. @WindowingMode private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED; @@ -208,17 +187,17 @@ class TaskFragmentContainer { @NonNull SplitController controller, @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag, @Nullable Bundle launchOptions, @Nullable Activity associatedActivity) { + mParcelableData = new ParcelableTaskFragmentContainerData( + new Binder("TaskFragmentContainer"), overlayTag, + associatedActivity != null ? associatedActivity.getActivityToken() : null); + if ((pendingAppearedActivity == null && pendingAppearedIntent == null) || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { throw new IllegalArgumentException( "One and only one of pending activity and intent must be non-null"); } mController = controller; - mToken = new Binder("TaskFragmentContainer"); mTaskContainer = taskContainer; - mOverlayTag = overlayTag; - mAssociatedActivityToken = associatedActivity != null - ? associatedActivity.getActivityToken() : null; if (launchOptions != null) { mLaunchOptions.putAll(launchOptions); @@ -259,18 +238,26 @@ class TaskFragmentContainer { if (overlayTag != null && pendingAppearedIntent != null && associatedActivity != null && !associatedActivity.isFinishing()) { final IBinder associatedActivityToken = associatedActivity.getActivityToken(); - final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams(mToken, - launchOptions, pendingAppearedIntent); + final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams( + mParcelableData.mToken, launchOptions, pendingAppearedIntent); mController.mOverlayRestoreParams.put(associatedActivityToken, params); } } + /** This is only used when restoring it from a {@link ParcelableTaskFragmentContainerData}. */ + TaskFragmentContainer(@NonNull ParcelableTaskFragmentContainerData data, + @NonNull SplitController splitController, @NonNull TaskContainer taskContainer) { + mParcelableData = data; + mController = splitController; + mTaskContainer = taskContainer; + } + /** * Returns the client-created token that uniquely identifies this container. */ @NonNull IBinder getTaskFragmentToken() { - return mToken; + return mParcelableData.mToken; } /** List of non-finishing activities that belong to this container and live in this process. */ @@ -389,7 +376,8 @@ class TaskFragmentContainer { return null; } return new ActivityStack(activities, isEmpty(), - ActivityStack.Token.createFromBinder(mToken), mOverlayTag); + ActivityStack.Token.createFromBinder(mParcelableData.mToken), + mParcelableData.mOverlayTag); } /** Adds the activity that will be reparented to this container. */ @@ -413,7 +401,7 @@ class TaskFragmentContainer { final ActivityThread.ActivityClientRecord record = ActivityThread .currentActivityThread().getActivityClient(activityToken); if (record != null) { - record.mTaskFragmentToken = mToken; + record.mTaskFragmentToken = mParcelableData.mToken; } } @@ -469,7 +457,7 @@ class TaskFragmentContainer { if (!isOverlayWithActivityAssociation()) { return; } - if (mAssociatedActivityToken == activityToken) { + if (mParcelableData.mAssociatedActivityToken == activityToken) { // If the associated activity is destroyed, also finish this overlay container. mController.mPresenter.cleanupContainer(wct, this, false /* shouldFinishDependent */); } @@ -776,8 +764,8 @@ class TaskFragmentContainer { * @see WindowContainerTransaction#setRelativeBounds */ boolean areLastRequestedBoundsEqual(@Nullable Rect relBounds) { - return (relBounds == null && mLastRequestedBounds.isEmpty()) - || mLastRequestedBounds.equals(relBounds); + return (relBounds == null && mParcelableData.mLastRequestedBounds.isEmpty()) + || mParcelableData.mLastRequestedBounds.equals(relBounds); } /** @@ -787,14 +775,14 @@ class TaskFragmentContainer { */ void setLastRequestedBounds(@Nullable Rect relBounds) { if (relBounds == null) { - mLastRequestedBounds.setEmpty(); + mParcelableData.mLastRequestedBounds.setEmpty(); } else { - mLastRequestedBounds.set(relBounds); + mParcelableData.mLastRequestedBounds.set(relBounds); } } @NonNull Rect getLastRequestedBounds() { - return mLastRequestedBounds; + return mParcelableData.mLastRequestedBounds; } /** @@ -965,6 +953,16 @@ class TaskFragmentContainer { return mTaskContainer.getTaskId(); } + @NonNull + IBinder getToken() { + return mParcelableData.mToken; + } + + @NonNull + ParcelableTaskFragmentContainerData getParcelableData() { + return mParcelableData; + } + /** Gets the parent Task. */ @NonNull TaskContainer getTaskContainer() { @@ -1011,7 +1009,7 @@ class TaskFragmentContainer { /** Returns whether this taskFragment container is an overlay container. */ boolean isOverlay() { - return mOverlayTag != null; + return mParcelableData.mOverlayTag != null; } /** @@ -1020,7 +1018,7 @@ class TaskFragmentContainer { */ @Nullable String getOverlayTag() { - return mOverlayTag; + return mParcelableData.mOverlayTag; } /** @@ -1045,7 +1043,7 @@ class TaskFragmentContainer { */ @Nullable IBinder getAssociatedActivityToken() { - return mAssociatedActivityToken; + return mParcelableData.mAssociatedActivityToken; } /** @@ -1053,11 +1051,11 @@ class TaskFragmentContainer { * a non-fill-parent overlay without activity association. */ boolean isAlwaysOnTopOverlay() { - return isOverlay() && mAssociatedActivityToken == null; + return isOverlay() && mParcelableData.mAssociatedActivityToken == null; } boolean isOverlayWithActivityAssociation() { - return isOverlay() && mAssociatedActivityToken != null; + return isOverlay() && mParcelableData.mAssociatedActivityToken != null; } @Override @@ -1074,13 +1072,13 @@ class TaskFragmentContainer { private String toString(boolean includeContainersToFinishOnExit) { return "TaskFragmentContainer{" + " parentTaskId=" + getTaskId() - + " token=" + mToken + + " token=" + mParcelableData.mToken + " topNonFinishingActivity=" + getTopNonFinishingActivity() + " runningActivityCount=" + getRunningActivityCount() + " isFinished=" + mIsFinished - + " overlayTag=" + mOverlayTag - + " associatedActivityToken=" + mAssociatedActivityToken - + " lastRequestedBounds=" + mLastRequestedBounds + + " overlayTag=" + mParcelableData.mOverlayTag + + " associatedActivityToken=" + mParcelableData.mAssociatedActivityToken + + " lastRequestedBounds=" + mParcelableData.mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" + containersToFinishOnExitToString() : "") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt index 423fe579a29a..cc47dbb78af2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellCoroutinesModule.kt @@ -61,20 +61,20 @@ class WMShellCoroutinesModule { @WMSingleton @ShellMainThread fun provideApplicationScope( - @ShellMainThread applicationDispatcher: CoroutineDispatcher, + @ShellMainThread applicationDispatcher: MainCoroutineDispatcher, ): CoroutineScope = CoroutineScope(applicationDispatcher) @Provides @WMSingleton @ShellBackgroundThread fun provideBackgroundCoroutineScope( - @ShellBackgroundThread backgroundDispatcher: CoroutineDispatcher, + @ShellBackgroundThread backgroundDispatcher: MainCoroutineDispatcher, ): CoroutineScope = CoroutineScope(backgroundDispatcher) @Provides @WMSingleton @ShellBackgroundThread fun provideBackgroundCoroutineContext( - @ShellBackgroundThread backgroundDispatcher: CoroutineDispatcher + @ShellBackgroundThread backgroundDispatcher: MainCoroutineDispatcher ): CoroutineContext = backgroundDispatcher + SupervisorJob() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index b68b436f2c1b..c8ffe28da79c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -171,6 +171,18 @@ fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { minOf(appBounds.height(), appBounds.width()).toFloat() } +/** Returns true if task's width or height is maximized else returns false. */ +fun isTaskWidthOrHeightEqual(taskBounds: Rect, stableBounds: Rect): Boolean { + return taskBounds.width() == stableBounds.width() || + taskBounds.height() == stableBounds.height() +} + +/** Returns true if task bound is equal to stable bounds else returns false. */ +fun isTaskBoundsEqual(taskBounds: Rect, stableBounds: Rect): Boolean { + return taskBounds.width() == stableBounds.width() && + taskBounds.height() == stableBounds.height() +} + /** * Calculates the desired initial bounds for applications in desktop windowing. This is done as a * scale of the screen bounds. 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 2852631656b5..90f8276240a7 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 @@ -169,6 +169,9 @@ class DesktopTasksController( } } + @VisibleForTesting + var taskbarDesktopTaskListener: TaskbarDesktopTaskListener? = null + /** Task id of the task currently being dragged from fullscreen/split. */ val draggingTaskId get() = dragToDesktopTransitionHandler.draggingTaskId @@ -610,13 +613,10 @@ class DesktopTasksController( val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds val destinationBounds = Rect() - val isMaximized = if (taskInfo.isResizeable) { - currentTaskBounds == stableBounds - } else { - currentTaskBounds.width() == stableBounds.width() - || currentTaskBounds.height() == stableBounds.height() - } - + val isMaximized = isTaskMaximized(taskInfo, stableBounds) + // If the task is currently maximized, we will toggle it not to be and vice versa. This is + // helpful to eliminate the current task from logic to calculate taskbar corner rounding. + val willMaximize = !isMaximized if (isMaximized) { // The desktop task is at the maximized width and/or height of the stable bounds. // If the task's pre-maximize stable bounds were saved, toggle the task to those bounds. @@ -651,6 +651,18 @@ class DesktopTasksController( } } + + + val shouldRestoreToSnap = + isMaximized && isTaskSnappedToHalfScreen(taskInfo, destinationBounds) + + logD("willMaximize = %s", willMaximize) + logD("shouldRestoreToSnap = %s", shouldRestoreToSnap) + + val doesAnyTaskRequireTaskbarRounding = willMaximize || shouldRestoreToSnap || + doesAnyTaskRequireTaskbarRounding(taskInfo.displayId, taskInfo.taskId) + + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding) val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { toggleResizeDesktopTaskTransitionHandler.startTransition(wct) @@ -659,6 +671,65 @@ class DesktopTasksController( } } + private fun isTaskMaximized( + taskInfo: RunningTaskInfo, + stableBounds: Rect + ): Boolean { + val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds + + return if (taskInfo.isResizeable) { + isTaskBoundsEqual(currentTaskBounds, stableBounds) + } else { + isTaskWidthOrHeightEqual(currentTaskBounds, stableBounds) + } + } + + private fun isMaximizedToStableBoundsEdges( + taskInfo: RunningTaskInfo, + stableBounds: Rect + ): Boolean { + val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds + return isTaskBoundsEqual(currentTaskBounds, stableBounds) + } + + /** Returns if current task bound is snapped to half screen */ + private fun isTaskSnappedToHalfScreen( + taskInfo: RunningTaskInfo, + taskBounds: Rect = taskInfo.configuration.windowConfiguration.bounds + ): Boolean = + getSnapBounds(taskInfo, SnapPosition.LEFT) == taskBounds || + getSnapBounds(taskInfo, SnapPosition.RIGHT) == taskBounds + + @VisibleForTesting + fun doesAnyTaskRequireTaskbarRounding( + displayId: Int, + excludeTaskId: Int? = null, + ): Boolean { + val doesAnyTaskRequireTaskbarRounding = + taskRepository.getActiveNonMinimizedOrderedTasks(displayId) + // exclude current task since maximize/restore transition has not taken place yet. + .filterNot { taskId -> taskId == excludeTaskId } + .any { taskId -> + val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)!! + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) + val stableBounds = Rect().apply { displayLayout?.getStableBounds(this) } + logD("taskInfo = %s", taskInfo) + logD( + "isTaskSnappedToHalfScreen(taskInfo) = %s", + isTaskSnappedToHalfScreen(taskInfo) + ) + logD( + "isMaximizedToStableBoundsEdges(taskInfo, stableBounds) = %s", + isMaximizedToStableBoundsEdges(taskInfo, stableBounds) + ) + isTaskSnappedToHalfScreen(taskInfo) + || isMaximizedToStableBoundsEdges(taskInfo, stableBounds) + } + + logD("doesAnyTaskRequireTaskbarRounding = %s", doesAnyTaskRequireTaskbarRounding) + return doesAnyTaskRequireTaskbarRounding + } + /** * Quick-resize to the right or left half of the stable bounds. * @@ -673,9 +744,9 @@ class DesktopTasksController( position: SnapPosition ) { val destinationBounds = getSnapBounds(taskInfo, position) - if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true) val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds) @@ -806,6 +877,10 @@ class DesktopTasksController( .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } .reversed() // Start from the back so the front task is brought forward last .forEach { task -> wct.reorder(task.token, /* onTop= */ true) } + + taskbarDesktopTaskListener?. + onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding(displayId)) + return taskToMinimize } @@ -1156,6 +1231,12 @@ class DesktopTasksController( ) { wct.removeTask(task.token) } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding( + task.displayId, + task.id + ) + ) return if (wct.isEmpty) null else wct } @@ -1450,6 +1531,8 @@ class DesktopTasksController( } // A freeform drag-move ended, remove the indicator immediately. releaseVisualIndicator() + taskbarDesktopTaskListener + ?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding(taskInfo.displayId)) } /** @@ -1686,17 +1769,39 @@ class DesktopTasksController( } } + private val mTaskbarDesktopTaskListener: TaskbarDesktopTaskListener = + object : TaskbarDesktopTaskListener { + override fun onTaskbarCornerRoundingUpdate( + hasTasksRequiringTaskbarRounding: Boolean) { + ProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: onTaskbarCornerRoundingUpdate " + + "doesAnyTaskRequireTaskbarRounding=%s", + hasTasksRequiringTaskbarRounding + ) + + remoteListener.call { l -> + l.onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding) + } + } + } + init { remoteListener = SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( controller, { c -> - c.taskRepository.addVisibleTasksListener( - listener, - c.mainExecutor - ) + run { + c.taskRepository.addVisibleTasksListener(listener, c.mainExecutor) + c.taskbarDesktopTaskListener = mTaskbarDesktopTaskListener + } }, - { c -> c.taskRepository.removeVisibleTasksListener(listener) } + { c -> + run { + c.taskRepository.removeVisibleTasksListener(listener) + c.taskbarDesktopTaskListener = null + } + } ) } @@ -1779,6 +1884,16 @@ class DesktopTasksController( private const val TAG = "DesktopTasksController" } + /** Defines interface for classes that can listen to changes for task resize. */ + // TODO(b/343931111): Migrate to using TransitionObservers when ready + interface TaskbarDesktopTaskListener { + /** + * [hasTasksRequiringTaskbarRounding] is true when a task is either maximized or snapped + * left/right and rounded corners are enabled. + */ + fun onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding: Boolean) + } + /** The positions on a screen that a task can snap to. */ enum class SnapPosition { RIGHT, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl index 8ebdfdcf4731..c2acb87222d1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -27,4 +27,10 @@ interface IDesktopTaskListener { /** @deprecated this is no longer supported. */ oneway void onStashedChanged(int displayId, boolean stashed); + + /** + * Shows taskbar corner radius when running desktop tasks are updated if + * [hasTasksRequiringTaskbarRounding] is true. + */ + oneway void onTaskbarCornerRoundingUpdate(boolean hasTasksRequiringTaskbarRounding); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 4fc6c4489f2b..9b0fb20f9777 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -18,8 +18,8 @@ package com.android.wm.shell.transition; import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; import static android.app.ActivityOptions.ANIM_CUSTOM; -import static android.app.ActivityOptions.ANIM_NONE; import static android.app.ActivityOptions.ANIM_FROM_STYLE; +import static android.app.ActivityOptions.ANIM_NONE; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; import static android.app.ActivityOptions.ANIM_SCALE_UP; import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION; @@ -473,7 +473,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { change.getLeash(), startTransaction); } else if (isOnlyTranslucent && TransitionUtil.isOpeningType(info.getType()) - && TransitionUtil.isClosingType(mode)) { + && TransitionUtil.isClosingType(mode)) { // If there is a closing translucent task in an OPENING transition, we will // actually select a CLOSING animation, so move the closing task into // the animating part of the z-order. @@ -767,12 +767,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { a = mTransitionAnimation.loadKeyguardExitAnimation(flags, (changeFlags & FLAG_SHOW_WALLPAPER) != 0); } else if (type == TRANSIT_KEYGUARD_UNOCCLUDE) { - a = mTransitionAnimation.loadKeyguardUnoccludeAnimation(); + a = mTransitionAnimation.loadKeyguardUnoccludeAnimation(options.getUserId()); } else if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { if (isOpeningType) { - a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter); + a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter, options.getUserId()); } else { - a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter); + a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter, options.getUserId()); } } else if (changeMode == TRANSIT_CHANGE) { // In the absence of a specific adapter, we just want to keep everything stationary. @@ -783,9 +783,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } else if (overrideType == ANIM_CUSTOM && (!isTask || options.getOverrideTaskTransition())) { a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter - ? options.getEnterResId() : options.getExitResId()); + ? options.getEnterResId() : options.getExitResId(), options.getUserId()); } else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) { - a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(); + a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(options.getUserId()); } else if (overrideType == ANIM_CLIP_REVEAL) { a = mTransitionAnimation.createClipRevealAnimationLocked(type, wallpaperTransit, enter, endBounds, endBounds, options.getTransitionBounds()); @@ -905,9 +905,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final Rect bounds = change.getEndAbsBounds(); // Show the right drawable depending on the user we're transitioning to. final Drawable thumbnailDrawable = change.hasFlags(FLAG_CROSS_PROFILE_OWNER_THUMBNAIL) - ? mContext.getDrawable(R.drawable.ic_account_circle) - : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL) - ? mEnterpriseThumbnailDrawable : null; + ? mContext.getDrawable(R.drawable.ic_account_circle) + : change.hasFlags(FLAG_CROSS_PROFILE_WORK_THUMBNAIL) + ? mEnterpriseThumbnailDrawable : null; if (thumbnailDrawable == null) { return; } 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 61fc09b1a727..ac35459347c6 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 @@ -505,16 +505,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { if (decoration == null) { return; } - openInBrowser(uri); + openInBrowser(uri, decoration.getUser()); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); } - private void openInBrowser(Uri uri) { + private void openInBrowser(Uri uri, @NonNull UserHandle userHandle) { final Intent intent = Intent.makeMainSelectorActivity(ACTION_MAIN, CATEGORY_APP_BROWSER) .setData(uri) .addFlags(FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); + mContext.startActivityAsUser(intent, userHandle); } private void onToDesktop(int taskId, DesktopModeTransitionSource source) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 2bec3fa6418d..81251b81e239 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -54,6 +54,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.os.Trace; +import android.os.UserHandle; import android.util.Size; import android.util.Slog; import android.view.Choreographer; @@ -480,6 +481,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return mGenericLink; } + UserHandle getUser() { + return mUserContext.getUser(); + } + private void updateDragResizeListener(SurfaceControl oldDecorationSurface) { if (!isDragResizable(mTaskInfo)) { if (!mTaskInfo.positionInParent.equals(mPositionInParent)) { 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 1b885aa11387..507ad647a788 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 @@ -17,6 +17,7 @@ package com.android.wm.shell.flicker import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppLayerIncreasesInSize import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart @@ -24,6 +25,9 @@ import android.tools.flicker.assertors.assertions.AppWindowBecomesVisible import android.tools.flicker.assertors.assertions.AppWindowCoversLeftHalfScreenAtEnd import android.tools.flicker.assertors.assertions.AppWindowCoversRightHalfScreenAtEnd import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowHasMaxBoundsInOnlyOneDimension +import android.tools.flicker.assertors.assertions.AppWindowHasMaxDisplayHeight +import android.tools.flicker.assertors.assertions.AppWindowHasMaxDisplayWidth import android.tools.flicker.assertors.assertions.AppWindowHasSizeOfAtLeast import android.tools.flicker.assertors.assertions.AppWindowIsInvisibleAtEnd import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways @@ -243,5 +247,42 @@ class DesktopModeFlickerScenarios { AppWindowReturnsToStartBoundsAndPosition(NON_RESIZABLE_APP) ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) + + val MAXIMIZE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("MAXIMIZE_APP"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val MAXIMIZE_APP_NON_RESIZABLE = + FlickerConfigEntry( + scenarioId = ScenarioId("MAXIMIZE_APP_NON_RESIZABLE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = + AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + AppWindowMaintainsAspectRatioAlways(DESKTOP_MODE_APP), + AppWindowHasMaxBoundsInOnlyOneDimension(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppLandscape.kt new file mode 100644 index 000000000000..217956671554 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppLandscape.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_90 +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.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppLandscape : MaximizeAppWindow(rotation = ROTATION_90) { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizableLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizableLandscape.kt new file mode 100644 index 000000000000..b173a60132b2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizableLandscape.kt @@ -0,0 +1,53 @@ +/* + * 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.Rotation.ROTATION_90 +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.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP_NON_RESIZABLE +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize non-resizable app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, maintaining its aspect ratio, until + * filling the vertical or horizontal stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppNonResizableLandscape : MaximizeAppWindow( + rotation = ROTATION_90, + isResizable = false +) { + @ExpectedScenarios(["MAXIMIZE_APP_NON_RESIZABLE"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizablePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizablePortrait.kt new file mode 100644 index 000000000000..88888eee8378 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppNonResizablePortrait.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP_NON_RESIZABLE +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize non-resizable app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, maintaining its aspect ratio, until + * filling the vertical or horizontal stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppNonResizablePortrait : MaximizeAppWindow(isResizable = false) { + @ExpectedScenarios(["MAXIMIZE_APP_NON_RESIZABLE"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP_NON_RESIZABLE) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppPortrait.kt new file mode 100644 index 000000000000..b79fd203fe1e --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppPortrait.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by pressing the maximize button on the app header. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppPortrait : MaximizeAppWindow() { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWindow() = super.maximizeAppWindow() + + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUtilsTest.kt new file mode 100644 index 000000000000..8fab410adc30 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUtilsTest.kt @@ -0,0 +1,66 @@ +/* + * 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 + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeUtilsTest { + @Test + fun isTaskBoundsEqual_stableBoundsAreEqual_returnTrue() { + assertThat(isTaskBoundsEqual(task2Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskBoundsEqual_stableBoundsAreNotEqual_returnFalse() { + assertThat(isTaskBoundsEqual(task4Bounds, stableBounds)).isFalse() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundsAreEqual_returnTrue() { + assertThat(isTaskWidthOrHeightEqual(task2Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundWidthIsEquals_returnTrue() { + assertThat(isTaskWidthOrHeightEqual(task3Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundHeightIsEquals_returnTrue() { + assertThat(isTaskWidthOrHeightEqual(task3Bounds, stableBounds)).isTrue() + } + + @Test + fun isTaskWidthOrHeightEqual_stableBoundsWidthOrHeightAreNotEquals_returnFalse() { + assertThat(isTaskWidthOrHeightEqual(task1Bounds, stableBounds)).isTrue() + } + + private companion object { + val task1Bounds = Rect(0, 0, 0, 0) + val task2Bounds = Rect(1, 1, 1, 1) + val task3Bounds = Rect(0, 1, 0, 1) + val task4Bounds = Rect(1, 2, 2, 1) + val stableBounds = Rect(1, 1, 1, 1) + } +} 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 d2487209dcef..5474e539f286 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 @@ -84,6 +84,7 @@ import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition +import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask @@ -175,6 +176,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockSurface: SurfaceControl + @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener private lateinit var mockitoSession: StaticMockitoSession private lateinit var controller: DesktopTasksController @@ -237,6 +239,8 @@ class DesktopTasksControllerTest : ShellTestCase() { val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) recentsTransitionStateListener = captor.value + + controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener } private fun createController(): DesktopTasksController { @@ -282,6 +286,52 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun doesAnyTaskRequireTaskbarRounding_onlyFreeFormTaskIsRunning_returnFalse() { + setUpFreeformTask() + + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isFalse() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { + val task1 = setUpFreeformTask() + + val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + controller.toggleDesktopTaskSize(task1) + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + + assertThat(argumentCaptor.value).isTrue() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_fullScreenTaskIsRunning_returnTrue() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + setUpFreeformTask(bounds = stableBounds, active = true) + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFullScreenTask_returnFalse() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + val task1 = setUpFreeformTask(bounds = stableBounds, active = true) + + val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + controller.toggleDesktopTaskSize(task1) + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + + assertThat(argumentCaptor.value).isFalse() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_splitScreenTaskIsRunning_returnTrue() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)) + + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() + } + + + @Test fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) clearInvocations(shellInit) 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 1ea8e8a498bb..0b5c6784b73d 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 @@ -34,6 +34,7 @@ import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager import android.net.Uri import android.os.Handler +import android.os.UserHandle import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.CheckFlagsRule @@ -163,6 +164,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockWindowManager: IWindowManager @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser + @Mock private lateinit var mockUserHandle: UserHandle @Mock private lateinit var mockToast: Toast private val bgExecutor = TestShellExecutor() @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper @@ -892,12 +894,12 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { openInBrowserListenerCaptor.value.accept(uri) - verify(spyContext).startActivity(argThat { intent -> + verify(spyContext).startActivityAsUser(argThat { intent -> intent.data == uri && ((intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0) && intent.categories.contains(Intent.CATEGORY_LAUNCHER) && intent.action == Intent.ACTION_MAIN - }) + }, eq(mockUserHandle)) } @Test @@ -1108,6 +1110,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.isFocused).thenReturn(task.isFocused) + whenever(decoration.user).thenReturn(mockUserHandle) if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) { whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId)) .thenReturn(true) diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java index d14275ff2fd6..92f6eaf33c88 100644 --- a/media/java/android/media/tv/tuner/Tuner.java +++ b/media/java/android/media/tv/tuner/Tuner.java @@ -2533,16 +2533,19 @@ public class Tuner implements AutoCloseable { /** * Request a frontend by frontend type. * - * <p> This API is used if the applications want to select a frontend with desired type when - * there are multiple frontends of the same type is there before {@link tune}. The applied - * frontend will be one of the not in-use frontend. If all frontends are in-use, this API will - * reclaim and apply the frontend owned by the lowest priority client if current client has - * higher priority. Otherwise, this API will not apply any frontend and return - * {@link #RESULT_UNAVAILABLE}. + * <p> This API is used (before {@link #tune(FrontendSettings)}) if the applications want to + * select a frontend of a particular type for {@link #tune(FrontendSettings)} when there are + * multiple frontends of the same type present, allowing the system to select which one is + * applied. The applied frontend will be one of the not-in-use frontends. If all frontends are + * in-use, this API will reclaim and apply the frontend owned by the lowest priority client if + * current client has higher priority. Otherwise, this API will not apply any frontend and + * return {@link #RESULT_UNAVAILABLE}. * * @param desiredFrontendType the Type of the desired fronted. Should be one of * {@link android.media.tv.tuner.frontend.FrontendSettings.Type} * @return result status of open operation. + * @see #applyFrontend(FrontendInfo) + * @see #tune(FrontendSettings) */ @Result @FlaggedApi(Flags.FLAG_TUNER_W_APIS) diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 447e980adaee..5b6b6c0b192e 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -220,14 +220,15 @@ package android.nfc.cardemulation { field @Deprecated public static final String ACTION_CHANGE_DEFAULT = "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; field public static final String CATEGORY_OTHER = "other"; field public static final String CATEGORY_PAYMENT = "payment"; - field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final String DH = "DH"; - field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final String ESE = "ESE"; field public static final String EXTRA_CATEGORY = "category"; field public static final String EXTRA_SERVICE_COMPONENT = "component"; + field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DH = 0; // 0x0 + field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE = 1; // 0x1 + field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC = 2; // 0x2 + field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET = -1; // 0xffffffff field public static final int SELECTION_MODE_ALWAYS_ASK = 1; // 0x1 field public static final int SELECTION_MODE_ASK_IF_CONFLICT = 2; // 0x2 field public static final int SELECTION_MODE_PREFER_DEFAULT = 0; // 0x0 - field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final String UICC = "UICC"; } public abstract class HostApduService extends android.app.Service { diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index 25a01b9c1b8d..717e01e18dbd 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -94,7 +94,7 @@ package android.nfc.cardemulation { public final class CardEmulation { method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static android.content.ComponentName getPreferredPaymentService(@NonNull android.content.Context); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int); - method @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public void overrideRoutingTable(@NonNull android.app.Activity, @Nullable String, @Nullable String); + method @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public void overrideRoutingTable(@NonNull android.app.Activity, int, int); method @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public void recoverRoutingTable(@NonNull android.app.Activity); } diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 0605dbe130d3..497309c12d8a 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -18,12 +18,12 @@ package android.nfc.cardemulation; import android.Manifest; import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; -import android.annotation.StringDef; import android.annotation.SystemApi; import android.annotation.UserHandleAware; import android.annotation.UserIdInt; @@ -155,17 +155,23 @@ public final class CardEmulation { * Route to Device Host (DH). */ @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE) - public static final String DH = "DH"; + public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DH = 0; /** * Route to eSE. */ @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE) - public static final String ESE = "ESE"; + public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE = 1; /** * Route to UICC. */ @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE) - public static final String UICC = "UICC"; + public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC = 2; + + /** + * Route unset. + */ + @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE) + public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET = -1; static boolean sIsInitialized = false; static HashMap<Context, CardEmulation> sCardEmus = new HashMap<Context, CardEmulation>(); @@ -734,7 +740,7 @@ public final class CardEmulation { * * @return the preferred payment service description */ - @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @RequiresPermission(Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) @Nullable public CharSequence getDescriptionForPreferredPaymentService() { ApduServiceInfo serviceInfo = callServiceReturn(() -> @@ -884,10 +890,12 @@ public final class CardEmulation { } /** @hide */ - @StringDef({ - DH, - ESE, - UICC + @IntDef(prefix = "PROTOCOL_AND_TECHNOLOGY_ROUTE_", + value = { + PROTOCOL_AND_TECHNOLOGY_ROUTE_DH, + PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE, + PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC, + PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET }) @Retention(RetentionPolicy.SOURCE) public @interface ProtocolAndTechnologyRoute {} @@ -896,29 +904,32 @@ public final class CardEmulation { * Setting NFC controller routing table, which includes Protocol Route and Technology Route, * while this Activity is in the foreground. * - * The parameter set to null can be used to keep current values for that entry. Either + * The parameter set to {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET} + * can be used to keep current values for that entry. Either * Protocol Route or Technology Route should be override when calling this API, otherwise * throw {@link IllegalArgumentException}. * <p> * Example usage in an Activity that requires to set proto route to "ESE" and keep tech route: * <pre> * protected void onResume() { - * mNfcAdapter.overrideRoutingTable(this , "ESE" , null); + * mNfcAdapter.overrideRoutingTable( + * this, {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE}, null); * }</pre> * </p> * Also activities must call {@link #recoverRoutingTable(Activity)} * when it goes to the background. Only the package of the * currently preferred service (the service set as preferred by the current foreground * application via {@link CardEmulation#setPreferredService(Activity, ComponentName)} or the - * current Default Wallet Role Holder {@link android.app.role.RoleManager#ROLE_WALLET}), + * current Default Wallet Role Holder {@link RoleManager#ROLE_WALLET}), * otherwise a call to this method will fail and throw {@link SecurityException}. * @param activity The Activity that requests NFC controller routing table to be changed. - * @param protocol ISO-DEP route destination, which can be "DH" or "UICC" or "ESE". - * @param technology Tech-A, Tech-B and Tech-F route destination, which can be "DH" or "UICC" - * or "ESE". + * @param protocol ISO-DEP route destination, where the possible inputs are defined + * in {@link ProtocolAndTechnologyRoute}. + * @param technology Tech-A, Tech-B and Tech-F route destination, where the possible inputs + * are defined in {@link ProtocolAndTechnologyRoute} * @throws SecurityException if the caller is not the preferred NFC service * @throws IllegalArgumentException if the activity is not resumed or the caller is not in the - * foreground, or both protocol route and technology route are null. + * foreground. * <p> * This is a high risk API and only included to support mainline effort * @hide @@ -926,25 +937,36 @@ public final class CardEmulation { @SystemApi @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE) public void overrideRoutingTable( - @NonNull Activity activity, @ProtocolAndTechnologyRoute @Nullable String protocol, - @ProtocolAndTechnologyRoute @Nullable String technology) { + @NonNull Activity activity, @ProtocolAndTechnologyRoute int protocol, + @ProtocolAndTechnologyRoute int technology) { if (!activity.isResumed()) { throw new IllegalArgumentException("Activity must be resumed."); } - if (protocol == null && technology == null) { - throw new IllegalArgumentException(("Both Protocol and Technology are null.")); - } + String protocolRoute = switch (protocol) { + case PROTOCOL_AND_TECHNOLOGY_ROUTE_DH -> "DH"; + case PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE -> "ESE"; + case PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC -> "UICC"; + case PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET -> null; + default -> throw new IllegalStateException("Unexpected value: " + protocol); + }; + String technologyRoute = switch (technology) { + case PROTOCOL_AND_TECHNOLOGY_ROUTE_DH -> "DH"; + case PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE -> "ESE"; + case PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC -> "UICC"; + case PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET -> null; + default -> throw new IllegalStateException("Unexpected value: " + protocol); + }; callService(() -> sService.overrideRoutingTable( mContext.getUser().getIdentifier(), - protocol, - technology, + protocolRoute, + technologyRoute, mContext.getPackageName())); } /** * Restore the NFC controller routing table, - * which was changed by {@link #overrideRoutingTable(Activity, String, String)} + * which was changed by {@link #overrideRoutingTable(Activity, int, int)} * * @param activity The Activity that requested NFC controller routing table to be changed. * @throws IllegalArgumentException if the caller is not in the foreground. diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml new file mode 100644 index 000000000000..35517ea0ec11 --- /dev/null +++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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. + --> + +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="?androidprv:attr/colorAccentPrimary" /> + <corners android:radius="12dp" /> +</shape>
\ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml new file mode 100644 index 000000000000..18696c627ec6 --- /dev/null +++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight"> + <item android:drawable="@drawable/audio_sharing_rounded_bg"/> +</ripple>
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 055afedd9422..0ffb76307b3c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -1,5 +1,6 @@ package com.android.settingslib.bluetooth; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.UNKNOWN_VALUE_PLACEHOLDER; import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED; import android.annotation.SuppressLint; @@ -704,12 +705,50 @@ public class BluetoothUtils { return !sourceList.isEmpty() && sourceList.stream().anyMatch(BluetoothUtils::isConnected); } + /** + * Check if {@link BluetoothDevice} has a active local broadcast source. + * + * @param device The bluetooth device to check. + * @param localBtManager The BT manager to provide BT functions. + * @return Whether the device has a active local broadcast source. + */ + @WorkerThread + public static boolean hasActiveLocalBroadcastSourceForBtDevice( + @Nullable BluetoothDevice device, @Nullable LocalBluetoothManager localBtManager) { + LocalBluetoothLeBroadcastAssistant assistant = + localBtManager == null + ? null + : localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + LocalBluetoothLeBroadcast broadcast = + localBtManager == null + ? null + : localBtManager.getProfileManager().getLeAudioBroadcastProfile(); + if (device == null || assistant == null || broadcast == null) { + Log.d(TAG, "Skip check hasActiveLocalBroadcastSourceForBtDevice due to arg is null"); + return false; + } + List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(device); + int broadcastId = broadcast.getLatestBroadcastId(); + return !sourceList.isEmpty() + && broadcastId != UNKNOWN_VALUE_PLACEHOLDER + && sourceList.stream() + .anyMatch( + source -> isSourceMatched(source, broadcastId)); + } + /** Checks the connectivity status based on the provided broadcast receive state. */ @WorkerThread public static boolean isConnected(BluetoothLeBroadcastReceiveState state) { return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0); } + /** Checks if the broadcast id is matched based on the provided broadcast receive state. */ + @WorkerThread + public static boolean isSourceMatched( + @Nullable BluetoothLeBroadcastReceiveState state, int broadcastId) { + return state != null && state.getBroadcastId() == broadcastId; + } + /** * Checks if the Bluetooth device is an available hearing device, which means: 1) currently * connected 2) is Hearing Aid 3) connected profile match hearing aid related profiles (e.g. diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 26905b1d86d2..6f2567b9c5dc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -101,7 +101,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private static final int DEFAULT_CODE_MIN = 1000; // Order of this profile in device profiles list private static final int ORDINAL = 1; - private static final int UNKNOWN_VALUE_PLACEHOLDER = -1; + static final int UNKNOWN_VALUE_PLACEHOLDER = -1; private static final Uri[] SETTINGS_URIS = new Uri[] { Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME), diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java index 4f2329bb75c2..47a08eb9a72b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java @@ -136,12 +136,10 @@ public class PowerAllowlistBackend { return true; } - if (android.app.admin.flags.Flags.disallowUserControlBgUsageFix()) { - // App is subject to DevicePolicyManager.setUserControlDisabledPackages() policy. - final int userId = UserHandle.getUserId(uid); - if (mAppContext.getPackageManager().isPackageStateProtected(pkg, userId)) { - return true; - } + // App is subject to DevicePolicyManager.setUserControlDisabledPackages() policy. + final int userId = UserHandle.getUserId(uid); + if (mAppContext.getPackageManager().isPackageStateProtected(pkg, userId)) { + return true; } return false; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 3a7b0c7bb104..a0e764ad3d5d 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -16,6 +16,7 @@ package com.android.settingslib.bluetooth; import static com.android.settingslib.bluetooth.BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.UNKNOWN_VALUE_PLACEHOLDER; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_DETERMINING_ADVANCED_DETAILS_HEADER_WITH_METADATA; import static com.google.common.truth.Truth.assertThat; @@ -96,6 +97,7 @@ public class BluetoothUtilsTest { + "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>"; private static final String TEST_EXCLUSIVE_MANAGER_PACKAGE = "com.test.manager"; private static final String TEST_EXCLUSIVE_MANAGER_COMPONENT = "com.test.manager/.component"; + private static final int TEST_BROADCAST_ID = 25; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -671,6 +673,34 @@ public class BluetoothUtilsTest { } @Test + public void testHasActiveLocalBroadcastSourceForBtDevice_hasActiveLocalSource() { + when(mBroadcast.getLatestBroadcastId()).thenReturn(TEST_BROADCAST_ID); + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(TEST_BROADCAST_ID); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasActiveLocalBroadcastSourceForBtDevice( + mBluetoothDevice, mLocalBluetoothManager)) + .isTrue(); + } + + @Test + public void testHasActiveLocalBroadcastSourceForBtDevice_noActiveLocalSource() { + when(mLeBroadcastReceiveState.getBroadcastId()).thenReturn(UNKNOWN_VALUE_PLACEHOLDER); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(sourceList); + + assertThat( + BluetoothUtils.hasActiveLocalBroadcastSourceForBtDevice( + mBluetoothDevice, mLocalBluetoothManager)) + .isFalse(); + } + + + @Test public void isAvailableHearingDevice_isConnectedHearingAid_returnTure() { when(mCachedBluetoothDevice.isConnectedHearingAidDevice()).thenReturn(true); when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index 65937ea067c6..f6e10570e656 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -340,7 +340,8 @@ public class GlobalSettingsValidators { new String[] { String.valueOf(Global.Wearable.PAIRED_DEVICE_OS_TYPE_UNKNOWN), String.valueOf(Global.Wearable.PAIRED_DEVICE_OS_TYPE_ANDROID), - String.valueOf(Global.Wearable.PAIRED_DEVICE_OS_TYPE_IOS) + String.valueOf(Global.Wearable.PAIRED_DEVICE_OS_TYPE_IOS), + String.valueOf(Global.Wearable.PAIRED_DEVICE_OS_TYPE_NONE) })); VALIDATORS.put( Global.Wearable.COMPANION_BLE_ROLE, diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig index 95e4b593a72f..10d7352da7dc 100644 --- a/packages/SystemUI/aconfig/biometrics_framework.aconfig +++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig @@ -3,3 +3,12 @@ container: "system" # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. +flag { + name: "bp_icon_a11y" + namespace: "biometrics_framework" + description: "Fixes biometric prompt icon not working as button with a11y" + bug: "359423579" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 9d3a8273878d..df4b51a9b7b9 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -384,6 +384,13 @@ flag { } flag { + name: "status_bar_ron_chips" + namespace: "systemui" + description: "Show rich ongoing notifications as chips in the status bar" + bug: "361346412" +} + +flag { name: "compose_bouncer" namespace: "systemui" description: "Use the new compose bouncer in SystemUI" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 5a4020d3b1fb..270d751d2d3c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -76,7 +76,7 @@ constructor( modifier: Modifier, ) = BouncerScene( - viewModel = rememberViewModel { contentViewModelFactory.create() }, + viewModel = rememberViewModel("BouncerScene") { contentViewModelFactory.create() }, dialogFactory = dialogFactory, modifier = modifier, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index b0590e06d3bd..be6a0f9346a3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -567,16 +567,8 @@ private fun ScrollOnUpdatedLiveContentEffect( // Do nothing if there is no new live content val indexOfFirstUpdatedContent = newLiveContentKeys.indexOfFirst { !prevLiveContentKeys.contains(it) } - if (indexOfFirstUpdatedContent < 0) { - return@LaunchedEffect - } - - // Scroll if the live content is not visible - val lastVisibleItemIndex = gridState.layoutInfo.visibleItemsInfo.lastOrNull()?.index - if (lastVisibleItemIndex != null && indexOfFirstUpdatedContent > lastVisibleItemIndex) { - // Launching with a scope to prevent the job from being canceled in the case of a - // recomposition during scrolling - coroutineScope.launch { gridState.animateScrollToItem(indexOfFirstUpdatedContent) } + if (indexOfFirstUpdatedContent in 0 until gridState.firstVisibleItemIndex) { + gridState.scrollToItem(indexOfFirstUpdatedContent) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt index 672b8a703e2b..dbe75382556f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt @@ -50,7 +50,7 @@ class LockscreenContent( fun SceneScope.Content( modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel { viewModelFactory.create() } + val viewModel = rememberViewModel("LockscreenContent") { viewModelFactory.create() } val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle() if (!isContentVisible) { // If the content isn't supposed to be visible, show a large empty box as it's needed diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5f4dc6e77c63..18e1092fba2e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -83,7 +83,7 @@ constructor( fun SceneScope.HeadsUpNotifications() { SnoozeableHeadsUpNotificationSpace( stackScrollView = stackScrollView.get(), - viewModel = rememberViewModel { viewModelFactory.create() }, + viewModel = rememberViewModel("HeadsUpNotifications") { viewModelFactory.create() }, ) } @@ -107,7 +107,7 @@ constructor( ConstrainedNotificationStack( stackScrollView = stackScrollView.get(), - viewModel = rememberViewModel { viewModelFactory.create() }, + viewModel = rememberViewModel("Notifications") { viewModelFactory.create() }, modifier = modifier .fillMaxWidth() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 9e292d06613b..a2beba849b89 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -321,7 +321,9 @@ fun SceneScope.NotificationScrollingStack( val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() } // The height of the scrim visible on screen when it is in its resting (collapsed) state. - val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() } + val minVisibleScrimHeight: () -> Float = { + screenHeight - maxScrimTop() - with(density) { navBarHeight.toPx() } + } // we are not scrolled to the top unless the scrim is at its maximum offset. LaunchedEffect(viewModel, scrimOffset) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index 62213bd22cbd..e9c96eaafc74 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -81,9 +81,10 @@ constructor( override fun SceneScope.Content( modifier: Modifier, ) { - val notificationsPlaceholderViewModel = rememberViewModel { - notificationsPlaceholderViewModelFactory.create() - } + val notificationsPlaceholderViewModel = + rememberViewModel("NotificationsShadeScene") { + notificationsPlaceholderViewModelFactory.create() + } OverlayShade( modifier = modifier, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index 192162475c9f..671b0128b621 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -98,7 +98,7 @@ private fun SceneScope.stateForQuickSettingsContent( else -> QSSceneAdapter.State.CLOSED } } - is TransitionState.Transition.ChangeCurrentScene -> + is TransitionState.Transition.ChangeScene -> with(transitionState) { when { isSplitShade -> UnsquishingQS(squishiness) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index f11f8bb94a52..d372577142c1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -152,7 +152,9 @@ constructor( notificationStackScrollView = notificationStackScrollView.get(), viewModelFactory = contentViewModelFactory, notificationsPlaceholderViewModel = - rememberViewModel { notificationsPlaceholderViewModelFactory.create() }, + rememberViewModel("QuickSettingsScene-notifPlaceholderViewModel") { + notificationsPlaceholderViewModelFactory.create() + }, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, @@ -179,10 +181,11 @@ private fun SceneScope.QuickSettingsScene( ) { val cutoutLocation = LocalDisplayCutout.current.location - val viewModel = rememberViewModel { viewModelFactory.create() } - val brightnessMirrorViewModel = rememberViewModel { - viewModel.brightnessMirrorViewModelFactory.create() - } + val viewModel = rememberViewModel("QuickSettingsScene-viewModel") { viewModelFactory.create() } + val brightnessMirrorViewModel = + rememberViewModel("QuickSettingsScene-brightnessMirrorViewModel") { + viewModel.brightnessMirrorViewModelFactory.create() + } val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() val contentAlpha by animateFloatAsState( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt index b25773b68471..90d7da65b29f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt @@ -94,7 +94,8 @@ constructor( override fun SceneScope.Content( modifier: Modifier, ) { - val viewModel = rememberViewModel { contentViewModelFactory.create() } + val viewModel = + rememberViewModel("QuickSettingsShadeScene") { contentViewModelFactory.create() } OverlayShade( viewModelFactory = viewModel.overlayShadeViewModelFactory, lockscreenContent = lockscreenContent, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index d489d731b5fb..cbbace47ecc4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -75,7 +75,10 @@ constructor( Spacer(modifier.fillMaxSize()) SnoozeableHeadsUpNotificationSpace( stackScrollView = notificationStackScrolLView.get(), - viewModel = rememberViewModel { notificationsPlaceholderViewModelFactory.create() }, + viewModel = + rememberViewModel("GoneScene") { + notificationsPlaceholderViewModelFactory.create() + }, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt index 1fee8741fe48..022eb1f5ef77 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt @@ -5,10 +5,16 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.bouncer.ui.composable.Bouncer +const val FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION = 0.5f + fun TransitionBuilder.lockscreenToBouncerTransition() { spec = tween(durationMillis = 500) translate(Bouncer.Elements.Content, y = 300.dp) - fractionRange(end = 0.5f) { fade(Bouncer.Elements.Background) } - fractionRange(start = 0.5f) { fade(Bouncer.Elements.Content) } + fractionRange(end = FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION) { + fade(Bouncer.Elements.Background) + } + fractionRange(start = FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION) { + fade(Bouncer.Elements.Content) + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 445ffcb0c60c..595bbb035f21 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -69,7 +69,7 @@ fun SceneScope.OverlayShade( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - val viewModel = rememberViewModel { viewModelFactory.create() } + val viewModel = rememberViewModel("OverlayShade") { viewModelFactory.create() } val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle() Box(modifier) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 8c53740ebfd0..05a0119d68e4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -129,7 +129,7 @@ fun SceneScope.CollapsedShadeHeader( statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel { viewModelFactory.create() } + val viewModel = rememberViewModel("CollapsedShadeHeader") { viewModelFactory.create() } val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() if (isDisabled) { return @@ -287,7 +287,7 @@ fun SceneScope.ExpandedShadeHeader( statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { - val viewModel = rememberViewModel { viewModelFactory.create() } + val viewModel = rememberViewModel("ExpandedShadeHeader") { viewModelFactory.create() } val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle() if (isDisabled) { return 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 f8513a8c4dd4..b7c6edce12d7 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 @@ -182,9 +182,12 @@ constructor( ) = ShadeScene( notificationStackScrollView.get(), - viewModel = rememberViewModel { contentViewModelFactory.create() }, + viewModel = + rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() }, notificationsPlaceholderViewModel = - rememberViewModel { notificationsPlaceholderViewModelFactory.create() }, + rememberViewModel("ShadeScene-notifPlaceholderViewModel") { + notificationsPlaceholderViewModelFactory.create() + }, createTintedIconManager = tintedIconManagerFactory::create, createBatteryMeterViewController = batteryMeterViewControllerFactory::create, statusBarIconController = statusBarIconController, @@ -493,9 +496,10 @@ private fun SceneScope.SplitShade( } } - val brightnessMirrorViewModel = rememberViewModel { - viewModel.brightnessMirrorViewModelFactory.create() - } + val brightnessMirrorViewModel = + rememberViewModel("SplitShade-brightnessMirrorViewModel") { + viewModel.brightnessMirrorViewModelFactory.create() + } val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle() val contentAlpha by animateFloatAsState( 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 abe079a4ab64..e15bc1243dd9 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.ChangeCurrentScene? { +): TransitionState.Transition.ChangeScene? { val transitionState = layoutState.transitionState if (transitionState.currentScene == target) { // This can happen in 3 different situations, for which there isn't anything else to do: @@ -55,7 +55,7 @@ internal fun CoroutineScope.animateToScene( replacedTransition = null, ) } - is TransitionState.Transition.ChangeCurrentScene -> { + is TransitionState.Transition.ChangeScene -> { val isInitiatedByUserInput = transitionState.isInitiatedByUserInput // A transition is currently running: first check whether `transition.toScene` or @@ -139,7 +139,7 @@ private fun CoroutineScope.animateToScene( reversed: Boolean = false, fromScene: SceneKey = layoutState.transitionState.currentScene, chain: Boolean = true, -): TransitionState.Transition.ChangeCurrentScene { +): TransitionState.Transition.ChangeScene { val oneOffAnimation = OneOffAnimation() val targetProgress = if (reversed) 0f else 1f val transition = @@ -184,7 +184,7 @@ private class OneOffSceneTransition( override val isInitiatedByUserInput: Boolean, replacedTransition: TransitionState.Transition?, private val oneOffAnimation: OneOffAnimation, -) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene, replacedTransition) { +) : TransitionState.Transition.ChangeScene(fromScene, toScene, replacedTransition) { override val progress: Float get() = oneOffAnimation.progress 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 71ff8a85159c..37e4daafdc7b 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 @@ -109,7 +109,7 @@ internal class DraggableHandlerImpl( // 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) - val fromContent = swipeAnimation.currentContent + val fromContent = layoutImpl.content(swipeAnimation.currentContent) val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent) val currentScene = layoutImpl.state.currentScene val contentTransition = swipeAnimation.contentTransition @@ -145,7 +145,9 @@ internal class DraggableHandlerImpl( // We need to recompute the swipe results since this is a new gesture, and the // fromScene.userActions may have changed. val swipes = oldDragController.swipes - swipes.updateSwipesResults(fromContent = oldSwipeAnimation.fromContent) + swipes.updateSwipesResults( + fromContent = layoutImpl.content(oldSwipeAnimation.fromContent) + ) // A new gesture should always create a new SwipeAnimation. This way there cannot be // different gestures controlling the same transition. @@ -155,8 +157,10 @@ internal class DraggableHandlerImpl( val swipes = computeSwipes(startedPosition, pointersDown) val fromContent = layoutImpl.contentForUserActions() + + swipes.updateSwipesResults(fromContent) val result = - swipes.findUserActionResult(fromContent, overSlop, updateSwipesResults = true) + swipes.findUserActionResult(overSlop) // As we were unable to locate a valid target scene, the initial SwipeAnimation // cannot be defined. Consequently, a simple NoOp Controller will be returned. ?: return NoOpDragController @@ -188,7 +192,13 @@ internal class DraggableHandlerImpl( else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)") } - return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation) + return createSwipeAnimation( + layoutImpl, + layoutImpl.coroutineScope, + result, + isUpOrLeft, + orientation + ) } private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes { @@ -291,7 +301,7 @@ private class DragControllerImpl( return onDrag(delta, swipeAnimation) } - private fun <T : Content> onDrag(delta: Float, swipeAnimation: SwipeAnimation<T>): Float { + private fun <T : ContentKey> onDrag(delta: Float, swipeAnimation: SwipeAnimation<T>): Float { if (delta == 0f || !isDrivingTransition || swipeAnimation.isFinishing) { return 0f } @@ -304,12 +314,12 @@ private class DragControllerImpl( fun hasReachedToSceneUpOrLeft() = distance < 0 && desiredOffset <= distance && - swipes.upOrLeftResult?.toContent(layoutState.currentScene) == toContent.key + swipes.upOrLeftResult?.toContent(layoutState.currentScene) == toContent fun hasReachedToSceneDownOrRight() = distance > 0 && desiredOffset >= distance && - swipes.downOrRightResult?.toContent(layoutState.currentScene) == toContent.key + swipes.downOrRightResult?.toContent(layoutState.currentScene) == toContent // Considering accelerated swipe: Change fromContent in the case where the user quickly // swiped multiple times in the same direction to accelerate the transition from A => B then @@ -321,7 +331,7 @@ private class DragControllerImpl( swipeAnimation.currentContent == toContent && (hasReachedToSceneUpOrLeft() || hasReachedToSceneDownOrRight()) - val fromContent: Content + val fromContent: ContentKey val currentTransitionOffset: Float val newOffset: Float val consumedDelta: Float @@ -357,12 +367,10 @@ private class DragControllerImpl( swipeAnimation.dragOffset = currentTransitionOffset - val result = - swipes.findUserActionResult( - fromContent = fromContent, - directionOffset = newOffset, - updateSwipesResults = hasReachedToContent - ) + if (hasReachedToContent) { + swipes.updateSwipesResults(draggableHandler.layoutImpl.content(fromContent)) + } + val result = swipes.findUserActionResult(directionOffset = newOffset) if (result == null) { onStop(velocity = delta, canChangeContent = true) @@ -371,7 +379,7 @@ private class DragControllerImpl( val needNewTransition = hasReachedToContent || - result.toContent(layoutState.currentScene) != swipeAnimation.toContent.key || + result.toContent(layoutState.currentScene) != swipeAnimation.toContent || result.transitionKey != swipeAnimation.contentTransition.key if (needNewTransition) { @@ -390,7 +398,7 @@ private class DragControllerImpl( return onStop(velocity, canChangeContent, swipeAnimation) } - private fun <T : Content> onStop( + private fun <T : ContentKey> onStop( velocity: Float, canChangeContent: Boolean, @@ -407,7 +415,6 @@ private class DragControllerImpl( fun animateTo(targetContent: T) { swipeAnimation.animateOffset( - coroutineScope = draggableHandler.coroutineScope, initialVelocity = velocity, targetContent = targetContent, ) @@ -518,6 +525,14 @@ internal class Swipes( return upOrLeftResult to downOrRightResult } + /** + * Update the swipes results. + * + * Usually we don't want to update them while doing a drag, because this could change the target + * content (jump cutting) to a different content, when some system state changed the targets the + * background. However, an update is needed any time we calculate the targets for a new + * fromContent. + */ fun updateSwipesResults(fromContent: Content) { val (upOrLeftResult, downOrRightResult) = computeSwipesResults(fromContent) @@ -526,31 +541,17 @@ internal class Swipes( } /** - * Returns the [UserActionResult] from [fromContent] in the direction of [directionOffset]. + * Returns the [UserActionResult] in the direction of [directionOffset]. * - * @param fromContent the content from which we look for the target * @param directionOffset signed float that indicates the direction. Positive is down or right * negative is up or left. - * @param updateSwipesResults whether the swipe results should be updated to the current values - * held in the user actions map. Usually we don't want to update them while doing a drag, - * because this could change the target content (jump cutting) to a different content, when - * some system state changed the targets the background. However, an update is needed any time - * we calculate the targets for a new fromContent. * @return null when there are no targets in either direction. If one direction is null and you * drag into the null direction this function will return the opposite direction, assuming * that the users intention is to start the drag into the other direction eventually. If * [directionOffset] is 0f and both direction are available, it will default to * [upOrLeftResult]. */ - fun findUserActionResult( - fromContent: Content, - directionOffset: Float, - updateSwipesResults: Boolean, - ): UserActionResult? { - if (updateSwipesResults) { - updateSwipesResults(fromContent) - } - + fun findUserActionResult(directionOffset: Float): UserActionResult? { return when { upOrLeftResult == null && downOrRightResult == null -> null (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt index 6181cfbb10eb..cb18c6729170 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt @@ -37,7 +37,7 @@ interface InterruptionHandler { * @see InterruptionResult */ fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey, ): InterruptionResult? } @@ -76,7 +76,7 @@ class InterruptionResult( */ object DefaultInterruptionHandler : InterruptionHandler { override fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey, ): InterruptionResult { return InterruptionResult( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt index 236e202749b2..3a7c2bf5d331 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt @@ -43,7 +43,7 @@ sealed interface ObservableTransitionState { fun currentScene(): Flow<SceneKey> { return when (this) { is Idle -> flowOf(currentScene) - is Transition.ChangeCurrentScene -> currentScene + is Transition.ChangeScene -> currentScene is Transition.ShowOrHideOverlay -> flowOf(currentScene) is Transition.ReplaceOverlay -> flowOf(currentScene) } @@ -94,7 +94,7 @@ sealed interface ObservableTransitionState { .trimMargin() /** A transition animating between [fromScene] and [toScene]. */ - class ChangeCurrentScene( + class ChangeScene( override val fromScene: SceneKey, override val toScene: SceneKey, val currentScene: Flow<SceneKey>, @@ -174,8 +174,8 @@ sealed interface ObservableTransitionState { previewProgress: Flow<Float> = flowOf(0f), isInPreviewStage: Flow<Boolean> = flowOf(false), currentOverlays: Flow<Set<OverlayKey>> = flowOf(emptySet()), - ): ChangeCurrentScene { - return ChangeCurrentScene( + ): ChangeScene { + return ChangeScene( fromScene, toScene, currentScene, @@ -190,7 +190,7 @@ sealed interface ObservableTransitionState { } } - fun isIdle(scene: SceneKey?): Boolean { + fun isIdle(scene: SceneKey? = null): Boolean { return this is Idle && (scene == null || this.currentScene == scene) } @@ -210,8 +210,8 @@ fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTrans return snapshotFlow { when (val state = transitionState) { is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene) - is TransitionState.Transition.ChangeCurrentScene -> { - ObservableTransitionState.Transition.ChangeCurrentScene( + is TransitionState.Transition.ChangeScene -> { + ObservableTransitionState.Transition.ChangeScene( fromScene = state.fromScene, toScene = state.toScene, currentScene = snapshotFlow { state.currentScene }, 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 e7e6b2a257d8..be4fea10602f 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -18,120 +18,75 @@ package com.android.compose.animation.scene import androidx.activity.BackEventCompat import androidx.activity.compose.PredictiveBackHandler -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.android.compose.animation.scene.content.state.TransitionState import kotlin.coroutines.cancellation.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch @Composable internal fun PredictiveBackHandler( - state: MutableSceneTransitionLayoutStateImpl, - coroutineScope: CoroutineScope, - targetSceneForBack: SceneKey? = null, + layoutImpl: SceneTransitionLayoutImpl, + result: UserActionResult?, ) { PredictiveBackHandler( - enabled = targetSceneForBack != null, + enabled = result != null, ) { progress: Flow<BackEventCompat> -> - val fromScene = state.transitionState.currentScene - if (targetSceneForBack == null || targetSceneForBack == fromScene) { + if (result == null) { // Note: We have to collect progress otherwise PredictiveBackHandler will throw. progress.first() return@PredictiveBackHandler } - val transition = - PredictiveBackTransition(state, coroutineScope, fromScene, toScene = targetSceneForBack) - state.startTransition(transition) - try { - progress.collect { backEvent -> transition.dragProgress = backEvent.progress } - - // Back gesture successful. - transition.animateTo(targetSceneForBack) - } catch (e: CancellationException) { - // Back gesture cancelled. - transition.animateTo(fromScene) - } + val animation = + createSwipeAnimation( + layoutImpl, + layoutImpl.coroutineScope, + result, + 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. + orientation = Orientation.Horizontal, + distance = 1f, + ) + + animate(layoutImpl, animation, progress) } } -private class PredictiveBackTransition( - val state: MutableSceneTransitionLayoutStateImpl, - val coroutineScope: CoroutineScope, - fromScene: SceneKey, - toScene: SceneKey, -) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene) { - override var currentScene by mutableStateOf(fromScene) - private set - - /** The animated progress once the gesture was committed or cancelled. */ - private var progressAnimatable by mutableStateOf<Animatable<Float, AnimationVector1D>?>(null) - var dragProgress: Float by mutableFloatStateOf(0f) - - override val previewProgress: Float - get() = dragProgress - - override val previewProgressVelocity: Float - get() = 0f // Currently, velocity is not exposed by predictive back API - - override val isInPreviewStage: Boolean - get() = previewTransformationSpec != null && currentScene == fromScene - - override val progress: Float - get() = progressAnimatable?.value ?: previewTransformationSpec?.let { 0f } ?: dragProgress - - override val progressVelocity: Float - get() = progressAnimatable?.velocity ?: 0f - - override val isInitiatedByUserInput: Boolean - get() = true - - override val isUserInputOngoing: Boolean - get() = progressAnimatable == null - - private var animationJob: Job? = null - - override fun finish(): Job = animateTo(currentScene) - - fun animateTo(scene: SceneKey): Job { - check(scene == fromScene || scene == toScene) - animationJob?.let { - return it +private suspend fun <T : ContentKey> animate( + layoutImpl: SceneTransitionLayoutImpl, + animation: SwipeAnimation<T>, + progress: Flow<BackEventCompat>, +) { + fun animateOffset(targetContent: T) { + if ( + layoutImpl.state.transitionState != animation.contentTransition || animation.isFinishing + ) { + return } - if (scene != currentScene && state.transitionState == this && state.canChangeScene(scene)) { - currentScene = scene - } + animation.animateOffset( + initialVelocity = 0f, + targetContent = targetContent, + + // TODO(b/350705972): Allow to customize or reuse the same customization endpoints as + // the normal swipe transitions. We can't just reuse them here because other swipe + // transitions animate pixels while this transition animates progress, so the visibility + // thresholds will be completely different. + spec = spring(), + ) + } - val targetProgress = - when (currentScene) { - fromScene -> 0f - toScene -> 1f - else -> error("scene $currentScene should be either $fromScene or $toScene") - } - val startProgress = if (previewTransformationSpec != null) 0f else dragProgress - val animatable = Animatable(startProgress).also { progressAnimatable = it } + layoutImpl.state.startTransition(animation.contentTransition) + try { + progress.collect { backEvent -> animation.dragOffset = backEvent.progress } - // 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. - return coroutineScope - .launch(start = CoroutineStart.ATOMIC) { - try { - animatable.animateTo(targetProgress) - } finally { - state.finishTransition(this@PredictiveBackTransition) - } - } - .also { animationJob = it } + // Back gesture successful. + animateOffset(animation.toContent) + } catch (e: CancellationException) { + // Back gesture cancelled. + animateOffset(animation.fromContent) } } 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 258be8122c1d..b33b4f6c5019 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 @@ -353,19 +353,8 @@ internal class SceneTransitionLayoutImpl( @Composable private fun BackHandler() { - val targetSceneForBack = - when (val result = contentForUserActions().userActions[Back.Resolved]) { - null -> null - is UserActionResult.ChangeScene -> result.toScene - is UserActionResult.ShowOverlay, - is UserActionResult.HideOverlay, - is UserActionResult.ReplaceByOverlay -> { - // TODO(b/353679003): Support overlay transitions when going back - null - } - } - - PredictiveBackHandler(state, coroutineScope, targetSceneForBack) + val result = contentForUserActions().userActions[Back.Resolved] + PredictiveBackHandler(layoutImpl = this, result = result) } @Composable @@ -389,7 +378,7 @@ internal class SceneTransitionLayoutImpl( // Compose the new scene we are going to first. transitions.fastForEachReversed { transition -> when (transition) { - is TransitionState.Transition.ChangeCurrentScene -> { + is TransitionState.Transition.ChangeScene -> { maybeAdd(transition.toScene) maybeAdd(transition.fromScene) } @@ -439,7 +428,7 @@ internal class SceneTransitionLayoutImpl( transitions.fastForEach { transition -> when (transition) { - is TransitionState.Transition.ChangeCurrentScene -> {} + is TransitionState.Transition.ChangeScene -> {} is TransitionState.Transition.ShowOrHideOverlay -> maybeAdd(transition.overlay) is TransitionState.Transition.ReplaceOverlay -> { @@ -495,7 +484,7 @@ private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) : val width: Int val height: Int val transition = - layoutImpl.state.currentTransition as? TransitionState.Transition.ChangeCurrentScene + layoutImpl.state.currentTransition as? TransitionState.Transition.ChangeScene if (transition == null) { width = placeable.width height = placeable.height 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 47065c7581fc..f3128f1bf5c7 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 @@ -299,7 +299,7 @@ internal class MutableSceneTransitionLayoutStateImpl( targetScene: SceneKey, coroutineScope: CoroutineScope, transitionKey: TransitionKey?, - ): TransitionState.Transition.ChangeCurrentScene? { + ): TransitionState.Transition.ChangeScene? { checkThread() return coroutineScope.animateToScene( 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 8ca90f18f3e0..57ff597d7314 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 @@ -18,15 +18,13 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.unit.IntSize -import com.android.compose.animation.scene.content.Content -import com.android.compose.animation.scene.content.Overlay -import com.android.compose.animation.scene.content.Scene 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 @@ -36,29 +34,96 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch internal fun createSwipeAnimation( + layoutState: MutableSceneTransitionLayoutStateImpl, + animationScope: CoroutineScope, + result: UserActionResult, + isUpOrLeft: Boolean, + orientation: Orientation, + distance: Float, +): SwipeAnimation<*> { + return createSwipeAnimation( + layoutState, + animationScope, + result, + isUpOrLeft, + orientation, + distance = { distance }, + contentForUserActions = { + error("Computing contentForUserActions requires a SceneTransitionLayoutImpl") + }, + ) +} + +internal fun createSwipeAnimation( layoutImpl: SceneTransitionLayoutImpl, + animationScope: CoroutineScope, + result: UserActionResult, + isUpOrLeft: Boolean, + orientation: Orientation, + distance: Float = DistanceUnspecified +): SwipeAnimation<*> { + var lastDistance = distance + + fun distance(animation: SwipeAnimation<*>): Float { + if (lastDistance != DistanceUnspecified) { + return lastDistance + } + + val absoluteDistance = + with(animation.contentTransition.transformationSpec.distance ?: DefaultSwipeDistance) { + layoutImpl.userActionDistanceScope.absoluteDistance( + layoutImpl.content(animation.fromContent).targetSize, + orientation, + ) + } + + if (absoluteDistance <= 0f) { + return DistanceUnspecified + } + + val distance = if (isUpOrLeft) -absoluteDistance else absoluteDistance + lastDistance = distance + return distance + } + + return createSwipeAnimation( + layoutImpl.state, + animationScope, + result, + isUpOrLeft, + orientation, + distance = ::distance, + contentForUserActions = { layoutImpl.contentForUserActions().key }, + ) +} + +private fun createSwipeAnimation( + layoutState: MutableSceneTransitionLayoutStateImpl, + animationScope: CoroutineScope, result: UserActionResult, isUpOrLeft: Boolean, orientation: Orientation, + distance: (SwipeAnimation<*>) -> Float, + contentForUserActions: () -> ContentKey, ): SwipeAnimation<*> { - fun <T : Content> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> { + fun <T : ContentKey> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> { return SwipeAnimation( - layoutImpl = layoutImpl, + layoutState = layoutState, + animationScope = animationScope, fromContent = fromContent, toContent = toContent, - userActionDistanceScope = layoutImpl.userActionDistanceScope, orientation = orientation, isUpOrLeft = isUpOrLeft, requiresFullDistanceSwipe = result.requiresFullDistanceSwipe, + distance = distance, ) } - val layoutState = layoutImpl.state return when (result) { is UserActionResult.ChangeScene -> { - val fromScene = layoutImpl.scene(layoutState.currentScene) - val toScene = layoutImpl.scene(result.toScene) - ChangeCurrentSceneSwipeTransition( + val fromScene = layoutState.currentScene + val toScene = result.toScene + ChangeSceneSwipeTransition( layoutState = layoutState, swipeAnimation = swipeAnimation(fromContent = fromScene, toContent = toScene), key = result.transitionKey, @@ -67,12 +132,12 @@ internal fun createSwipeAnimation( .swipeAnimation } is UserActionResult.ShowOverlay -> { - val fromScene = layoutImpl.scene(layoutState.currentScene) - val overlay = layoutImpl.overlay(result.overlay) + val fromScene = layoutState.currentScene + val overlay = result.overlay ShowOrHideOverlaySwipeTransition( layoutState = layoutState, - _fromOrToScene = fromScene, - _overlay = overlay, + fromOrToScene = fromScene, + overlay = overlay, swipeAnimation = swipeAnimation(fromContent = fromScene, toContent = overlay), key = result.transitionKey, replacedTransition = null, @@ -80,12 +145,12 @@ internal fun createSwipeAnimation( .swipeAnimation } is UserActionResult.HideOverlay -> { - val toScene = layoutImpl.scene(layoutState.currentScene) - val overlay = layoutImpl.overlay(result.overlay) + val toScene = layoutState.currentScene + val overlay = result.overlay ShowOrHideOverlaySwipeTransition( layoutState = layoutState, - _fromOrToScene = toScene, - _overlay = overlay, + fromOrToScene = toScene, + overlay = overlay, swipeAnimation = swipeAnimation(fromContent = overlay, toContent = toScene), key = result.transitionKey, replacedTransition = null, @@ -93,8 +158,14 @@ internal fun createSwipeAnimation( .swipeAnimation } is UserActionResult.ReplaceByOverlay -> { - val fromOverlay = layoutImpl.contentForUserActions() as Overlay - val toOverlay = layoutImpl.overlay(result.overlay) + val fromOverlay = + when (val contentForUserActions = contentForUserActions()) { + is SceneKey -> + error("ReplaceByOverlay can only be called when an overlay is shown") + is OverlayKey -> contentForUserActions + } + + val toOverlay = result.overlay ReplaceOverlaySwipeTransition( layoutState = layoutState, swipeAnimation = @@ -109,9 +180,8 @@ internal fun createSwipeAnimation( internal fun createSwipeAnimation(old: SwipeAnimation<*>): SwipeAnimation<*> { return when (val transition = old.contentTransition) { - is TransitionState.Transition.ChangeCurrentScene -> { - ChangeCurrentSceneSwipeTransition(transition as ChangeCurrentSceneSwipeTransition) - .swipeAnimation + is TransitionState.Transition.ChangeScene -> { + ChangeSceneSwipeTransition(transition as ChangeSceneSwipeTransition).swipeAnimation } is TransitionState.Transition.ShowOrHideOverlay -> { ShowOrHideOverlaySwipeTransition(transition as ShowOrHideOverlaySwipeTransition) @@ -125,15 +195,15 @@ internal fun createSwipeAnimation(old: SwipeAnimation<*>): SwipeAnimation<*> { } /** A helper class that contains the main logic for swipe transitions. */ -internal class SwipeAnimation<T : Content>( - val layoutImpl: SceneTransitionLayoutImpl, +internal class SwipeAnimation<T : ContentKey>( + val layoutState: MutableSceneTransitionLayoutStateImpl, + val animationScope: CoroutineScope, val fromContent: T, val toContent: T, - private val userActionDistanceScope: UserActionDistanceScope, override val orientation: Orientation, override val isUpOrLeft: Boolean, val requiresFullDistanceSwipe: Boolean, - private var lastDistance: Float = DistanceUnspecified, + private val distance: (SwipeAnimation<T>) -> Float, currentContent: T = fromContent, dragOffset: Float = 0f, ) : TransitionState.HasOverscrollProperties { @@ -147,7 +217,13 @@ internal class SwipeAnimation<T : Content>( // 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 offset = offsetAnimation?.animatable?.value ?: dragOffset + val animatable = offsetAnimation?.animatable + val offset = + when { + animatable != null -> animatable.value + contentTransition.previewTransformationSpec != null -> 0f + else -> dragOffset + } return computeProgress(offset) } @@ -172,6 +248,15 @@ internal class SwipeAnimation<T : Content>( return velocityInDistanceUnit / distance.absoluteValue } + val previewProgress: Float + get() = computeProgress(dragOffset) + + val previewProgressVelocity: Float + get() = 0f + + val isInPreviewStage: Boolean + get() = contentTransition.previewTransformationSpec != null && currentContent == fromContent + override var bouncingContent: ContentKey? = null /** The current offset caused by the drag gesture. */ @@ -183,17 +268,8 @@ internal class SwipeAnimation<T : Content>( val isUserInputOngoing: Boolean get() = offsetAnimation == null - override val overscrollScope: OverscrollScope = - object : OverscrollScope { - override val density: Float - get() = layoutImpl.density.density - - override val fontScale: Float - get() = layoutImpl.density.fontScale - - override val absoluteDistance: Float - get() = distance().absoluteValue - } + override val absoluteDistance: Float + get() = distance().absoluteValue /** Whether [finish] was called on this animation. */ var isFinishing = false @@ -202,14 +278,14 @@ internal class SwipeAnimation<T : Content>( constructor( other: SwipeAnimation<T> ) : this( - layoutImpl = other.layoutImpl, + layoutState = other.layoutState, + animationScope = other.animationScope, fromContent = other.fromContent, toContent = other.toContent, - userActionDistanceScope = other.userActionDistanceScope, orientation = other.orientation, isUpOrLeft = other.isUpOrLeft, requiresFullDistanceSwipe = other.requiresFullDistanceSwipe, - lastDistance = other.lastDistance, + distance = other.distance, currentContent = other.currentContent, dragOffset = other.dragOffset, ) @@ -222,27 +298,7 @@ internal class SwipeAnimation<T : Content>( * transition when the distance depends on the size or position of an element that is composed * in the content we are going to. */ - fun distance(): Float { - if (lastDistance != DistanceUnspecified) { - return lastDistance - } - - val absoluteDistance = - with(contentTransition.transformationSpec.distance ?: DefaultSwipeDistance) { - userActionDistanceScope.absoluteDistance( - fromContent.targetSize, - orientation, - ) - } - - if (absoluteDistance <= 0f) { - return DistanceUnspecified - } - - val distance = if (isUpOrLeft) -absoluteDistance else absoluteDistance - lastDistance = distance - return distance - } + fun distance(): Float = distance(this) /** Ends any previous [offsetAnimation] and runs the new [animation]. */ private fun startOffsetAnimation(animation: () -> OffsetAnimation): OffsetAnimation { @@ -262,10 +318,9 @@ internal class SwipeAnimation<T : Content>( } fun animateOffset( - // TODO(b/317063114) The CoroutineScope should be removed. - coroutineScope: CoroutineScope, initialVelocity: Float, targetContent: T, + spec: SpringSpec<Float>? = null, ): OffsetAnimation { val initialProgress = progress // Skip the animation if we have already reached the target content and the overscroll does @@ -304,12 +359,14 @@ internal class SwipeAnimation<T : Content>( } return startOffsetAnimation { - val animatable = Animatable(dragOffset, OffsetVisibilityThreshold) + 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 = - coroutineScope + 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 @@ -325,8 +382,9 @@ internal class SwipeAnimation<T : Content>( try { val swipeSpec = - contentTransition.transformationSpec.swipeSpec - ?: layoutImpl.state.transitions.defaultSwipeSpec + spec + ?: contentTransition.transformationSpec.swipeSpec + ?: layoutState.transitions.defaultSwipeSpec animatable.animateTo( targetValue = targetOffset, animationSpec = swipeSpec, @@ -349,7 +407,7 @@ internal class SwipeAnimation<T : Content>( } if (isBouncing) { - bouncingContent = targetContent.key + bouncingContent = targetContent // Immediately stop this transition if we are bouncing on a // content that does not bounce. @@ -368,20 +426,19 @@ internal class SwipeAnimation<T : Content>( } } - private fun canChangeContent(targetContent: Content): Boolean { - val layoutState = layoutImpl.state + private fun canChangeContent(targetContent: ContentKey): Boolean { return when (val transition = contentTransition) { - is TransitionState.Transition.ChangeCurrentScene -> - layoutState.canChangeScene(targetContent.key as SceneKey) + is TransitionState.Transition.ChangeScene -> + layoutState.canChangeScene(targetContent as SceneKey) is TransitionState.Transition.ShowOrHideOverlay -> { - if (targetContent.key == transition.overlay) { + if (targetContent == transition.overlay) { layoutState.canShowOverlay(transition.overlay) } else { layoutState.canHideOverlay(transition.overlay) } } is TransitionState.Transition.ReplaceOverlay -> { - val to = targetContent.key as OverlayKey + val to = targetContent as OverlayKey val from = if (to == transition.toOverlay) transition.fromOverlay else transition.toOverlay layoutState.canReplaceOverlay(from, to) @@ -392,7 +449,7 @@ internal class SwipeAnimation<T : Content>( private fun snapToContent(content: T) { cancelOffsetAnimation() check(currentContent == content) - layoutImpl.state.finishTransition(contentTransition) + layoutState.finishTransition(contentTransition) } fun finish(): Job { @@ -405,12 +462,7 @@ internal class SwipeAnimation<T : Content>( } // Animate to the current content. - val animation = - animateOffset( - coroutineScope = layoutImpl.coroutineScope, - initialVelocity = 0f, - targetContent = currentContent, - ) + val animation = animateOffset(initialVelocity = 0f, targetContent = currentContent) check(offsetAnimation == animation) return animation.job } @@ -436,21 +488,21 @@ private object DefaultSwipeDistance : UserActionDistance { } } -private class ChangeCurrentSceneSwipeTransition( +private class ChangeSceneSwipeTransition( val layoutState: MutableSceneTransitionLayoutStateImpl, - val swipeAnimation: SwipeAnimation<Scene>, + val swipeAnimation: SwipeAnimation<SceneKey>, override val key: TransitionKey?, - replacedTransition: ChangeCurrentSceneSwipeTransition?, + replacedTransition: ChangeSceneSwipeTransition?, ) : - TransitionState.Transition.ChangeCurrentScene( - swipeAnimation.fromContent.key, - swipeAnimation.toContent.key, + TransitionState.Transition.ChangeScene( + swipeAnimation.fromContent, + swipeAnimation.toContent, replacedTransition, ), TransitionState.HasOverscrollProperties by swipeAnimation { constructor( - other: ChangeCurrentSceneSwipeTransition + other: ChangeSceneSwipeTransition ) : this( layoutState = other.layoutState, swipeAnimation = SwipeAnimation(other.swipeAnimation), @@ -463,7 +515,7 @@ private class ChangeCurrentSceneSwipeTransition( } override val currentScene: SceneKey - get() = swipeAnimation.currentContent.key + get() = swipeAnimation.currentContent override val progress: Float get() = swipeAnimation.progress @@ -471,6 +523,15 @@ private class ChangeCurrentSceneSwipeTransition( override val progressVelocity: Float get() = swipeAnimation.progressVelocity + override val previewProgress: Float + get() = swipeAnimation.previewProgress + + override val previewProgressVelocity: Float + get() = swipeAnimation.previewProgressVelocity + + override val isInPreviewStage: Boolean + get() = swipeAnimation.isInPreviewStage + override val isInitiatedByUserInput: Boolean = true override val isUserInputOngoing: Boolean @@ -481,17 +542,17 @@ private class ChangeCurrentSceneSwipeTransition( private class ShowOrHideOverlaySwipeTransition( val layoutState: MutableSceneTransitionLayoutStateImpl, - val swipeAnimation: SwipeAnimation<Content>, - val _overlay: Overlay, - val _fromOrToScene: Scene, + val swipeAnimation: SwipeAnimation<ContentKey>, + overlay: OverlayKey, + fromOrToScene: SceneKey, override val key: TransitionKey?, replacedTransition: ShowOrHideOverlaySwipeTransition?, ) : TransitionState.Transition.ShowOrHideOverlay( - _overlay.key, - _fromOrToScene.key, - swipeAnimation.fromContent.key, - swipeAnimation.toContent.key, + overlay, + fromOrToScene, + swipeAnimation.fromContent, + swipeAnimation.toContent, replacedTransition, ), TransitionState.HasOverscrollProperties by swipeAnimation { @@ -500,8 +561,8 @@ private class ShowOrHideOverlaySwipeTransition( ) : this( layoutState = other.layoutState, swipeAnimation = SwipeAnimation(other.swipeAnimation), - _overlay = other._overlay, - _fromOrToScene = other._fromOrToScene, + overlay = other.overlay, + fromOrToScene = other.fromOrToScene, key = other.key, replacedTransition = other, ) @@ -511,7 +572,7 @@ private class ShowOrHideOverlaySwipeTransition( } override val isEffectivelyShown: Boolean - get() = swipeAnimation.currentContent == _overlay + get() = swipeAnimation.currentContent == overlay override val progress: Float get() = swipeAnimation.progress @@ -519,6 +580,15 @@ private class ShowOrHideOverlaySwipeTransition( override val progressVelocity: Float get() = swipeAnimation.progressVelocity + override val previewProgress: Float + get() = swipeAnimation.previewProgress + + override val previewProgressVelocity: Float + get() = swipeAnimation.previewProgressVelocity + + override val isInPreviewStage: Boolean + get() = swipeAnimation.isInPreviewStage + override val isInitiatedByUserInput: Boolean = true override val isUserInputOngoing: Boolean @@ -529,13 +599,13 @@ private class ShowOrHideOverlaySwipeTransition( private class ReplaceOverlaySwipeTransition( val layoutState: MutableSceneTransitionLayoutStateImpl, - val swipeAnimation: SwipeAnimation<Overlay>, + val swipeAnimation: SwipeAnimation<OverlayKey>, override val key: TransitionKey?, replacedTransition: ReplaceOverlaySwipeTransition?, ) : TransitionState.Transition.ReplaceOverlay( - swipeAnimation.fromContent.key, - swipeAnimation.toContent.key, + swipeAnimation.fromContent, + swipeAnimation.toContent, replacedTransition, ), TransitionState.HasOverscrollProperties by swipeAnimation { @@ -553,7 +623,7 @@ private class ReplaceOverlaySwipeTransition( } override val effectivelyShownOverlay: OverlayKey - get() = swipeAnimation.currentContent.key + get() = swipeAnimation.currentContent override val progress: Float get() = swipeAnimation.progress @@ -561,6 +631,15 @@ private class ReplaceOverlaySwipeTransition( override val progressVelocity: Float get() = swipeAnimation.progressVelocity + override val previewProgress: Float + get() = swipeAnimation.previewProgress + + override val previewProgressVelocity: Float + get() = swipeAnimation.previewProgressVelocity + + override val isInPreviewStage: Boolean + get() = swipeAnimation.isInPreviewStage + override val isInitiatedByUserInput: Boolean = true override val isUserInputOngoing: Boolean 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 fdb019f5a604..0cd8c1af0507 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 @@ -26,7 +26,6 @@ import androidx.compose.runtime.getValue import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.OverlayKey -import com.android.compose.animation.scene.OverscrollScope import com.android.compose.animation.scene.OverscrollSpecImpl import com.android.compose.animation.scene.ProgressVisibilityThreshold import com.android.compose.animation.scene.SceneKey @@ -75,7 +74,7 @@ sealed interface TransitionState { val replacedTransition: Transition? = null, ) : TransitionState { /** A transition animating between [fromScene] and [toScene]. */ - abstract class ChangeCurrentScene( + abstract class ChangeScene( /** The scene this transition is starting from. Can't be the same as toScene */ val fromScene: SceneKey, @@ -386,10 +385,10 @@ sealed interface TransitionState { val orientation: Orientation /** - * Scope which can be used in the Overscroll DSL to define a transformation based on the - * distance between [Transition.fromContent] and [Transition.toContent]. + * Return the absolute distance between fromScene and toScene, if available, otherwise + * [DistanceUnspecified]. */ - val overscrollScope: OverscrollScope + val absoluteDistance: Float /** * The content (scene or overlay) around which the transition is currently bouncing. When diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index 59bca50f7d5b..8f845866a0f3 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -17,6 +17,7 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentKey @@ -53,6 +54,8 @@ internal class OverscrollTranslate( val x: OverscrollScope.() -> Float = { 0f }, val y: OverscrollScope.() -> Float = { 0f }, ) : PropertyTransformation<Offset> { + private val cachedOverscrollScope = CachedOverscrollScope() + override fun transform( layoutImpl: SceneTransitionLayoutImpl, content: ContentKey, @@ -65,10 +68,47 @@ internal class OverscrollTranslate( // OverscrollSpec only when the transition implements HasOverscrollProperties, we can assume // that this method was invoked after performing this check. val overscrollProperties = transition as TransitionState.HasOverscrollProperties + val overscrollScope = + cachedOverscrollScope.getFromCacheOrCompute(layoutImpl.density, overscrollProperties) return Offset( - x = value.x + overscrollProperties.overscrollScope.x(), - y = value.y + overscrollProperties.overscrollScope.y(), + x = value.x + overscrollScope.x(), + y = value.y + overscrollScope.y(), ) } } + +/** + * A helper class to cache a [OverscrollScope] given a [Density] and + * [TransitionState.HasOverscrollProperties]. This helps avoid recreating a scope every frame + * whenever an overscroll transition is computed. + */ +private class CachedOverscrollScope() { + private var previousScope: OverscrollScope? = null + private var previousDensity: Density? = null + private var previousOverscrollProperties: TransitionState.HasOverscrollProperties? = null + + fun getFromCacheOrCompute( + density: Density, + overscrollProperties: TransitionState.HasOverscrollProperties, + ): OverscrollScope { + if ( + previousScope == null || + density != previousDensity || + previousOverscrollProperties != overscrollProperties + ) { + val scope = + object : OverscrollScope, Density by density { + override val absoluteDistance: Float + get() = overscrollProperties.absoluteDistance + } + + previousScope = scope + previousDensity = density + previousOverscrollProperties = overscrollProperties + return scope + } + + return checkNotNull(previousScope) + } +} 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 59ddb1354073..564d4b3a3c5a 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 @@ -27,7 +27,7 @@ internal class LinkedTransition( fromScene: SceneKey, toScene: SceneKey, override val key: TransitionKey? = null, -) : TransitionState.Transition.ChangeCurrentScene(fromScene, toScene) { +) : TransitionState.Transition.ChangeScene(fromScene, toScene) { override val currentScene: SceneKey get() { 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 f4e60a2a4100..3f6bd2c38792 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 @@ -69,7 +69,7 @@ class InterruptionHandlerTest { interruptionHandler = object : InterruptionHandler { override fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey ): InterruptionResult { return InterruptionResult( @@ -104,7 +104,7 @@ class InterruptionHandlerTest { interruptionHandler = object : InterruptionHandler { override fun onInterruption( - interrupted: TransitionState.Transition.ChangeCurrentScene, + interrupted: TransitionState.Transition.ChangeScene, newTargetScene: SceneKey ): InterruptionResult { return InterruptionResult( @@ -198,7 +198,7 @@ class InterruptionHandlerTest { companion object { val FromToCurrentTriple = Correspondence.transforming( - { transition: TransitionState.Transition.ChangeCurrentScene? -> + { transition: TransitionState.Transition.ChangeScene? -> Triple(transition?.fromScene, transition?.toScene, transition?.currentScene) }, "(from, to, current) triple" diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt index a549d0355a26..e4879d9d8a31 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt @@ -163,7 +163,7 @@ class MovableElementTest { fromContentZIndex: Float, toContentZIndex: Float ): ContentKey { - transition as TransitionState.Transition.ChangeCurrentScene + transition as TransitionState.Transition.ChangeScene assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) assertThat(fromContentZIndex).isEqualTo(0) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt index 00c75882a587..c5b6cdf12385 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt @@ -20,10 +20,16 @@ import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestOverlays.OverlayA +import com.android.compose.animation.scene.TestOverlays.OverlayB import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC @@ -198,6 +204,42 @@ class PredictiveBackHandlerTest { assertThat(canChangeSceneCalled).isFalse() } + @Test + fun backDismissesOverlayWithHighestZIndexByDefault() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + initialOverlays = setOf(OverlayA, OverlayB) + ) + } + + rule.setContent { + SceneTransitionLayout(state, Modifier.size(200.dp)) { + scene(SceneA) { Box(Modifier.fillMaxSize()) } + overlay(OverlayA) { Box(Modifier.fillMaxSize()) } + overlay(OverlayB) { Box(Modifier.fillMaxSize()) } + } + } + + // Initial state. + rule.onNode(hasTestTag(SceneA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayB.testTag)).assertIsDisplayed() + + // Press back. This should hide overlay B because it has a higher zIndex than overlay A. + rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() } + rule.onNode(hasTestTag(SceneA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayB.testTag)).assertDoesNotExist() + + // Press back again. This should hide overlay A. + rule.runOnUiThread { rule.activity.onBackPressedDispatcher.onBackPressed() } + rule.onNode(hasTestTag(SceneA.testTag)).assertIsDisplayed() + rule.onNode(hasTestTag(OverlayA.testTag)).assertDoesNotExist() + rule.onNode(hasTestTag(OverlayB.testTag)).assertDoesNotExist() + } + private fun backEvent(progress: Float = 0f): BackEventCompat { return BackEventCompat( touchX = 0f, 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/animation/scene/Transition.kt index 1f7fe3766971..467031afb262 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/animation/scene/Transition.kt @@ -42,9 +42,9 @@ fun transition( orientation: Orientation = Orientation.Horizontal, onFinish: ((TransitionState.Transition) -> Job)? = null, replacedTransition: TransitionState.Transition? = null, -): TransitionState.Transition.ChangeCurrentScene { +): TransitionState.Transition.ChangeScene { return object : - TransitionState.Transition.ChangeCurrentScene(from, to, replacedTransition), + TransitionState.Transition.ChangeScene(from, to, replacedTransition), TransitionState.HasOverscrollProperties { override val currentScene: SceneKey get() = current() @@ -69,12 +69,7 @@ fun transition( override val isUpOrLeft: Boolean = isUpOrLeft override val bouncingContent: ContentKey? = bouncingContent override val orientation: Orientation = orientation - override val overscrollScope: OverscrollScope = - object : OverscrollScope { - override val density: Float = 1f - override val fontScale: Float = 1f - override val absoluteDistance = 0f - } + override val absoluteDistance = 0f override fun finish(): Job { val onFinish = diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt index 3fb57084a461..44e0ba51f713 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt @@ -32,8 +32,8 @@ fun assertThat(state: TransitionState): TransitionStateSubject { return Truth.assertAbout(TransitionStateSubject.transitionStates()).that(state) } -/** Assert on a [TransitionState.Transition.ChangeCurrentScene]. */ -fun assertThat(transition: TransitionState.Transition.ChangeCurrentScene): SceneTransitionSubject { +/** Assert on a [TransitionState.Transition.ChangeScene]. */ +fun assertThat(transition: TransitionState.Transition.ChangeScene): SceneTransitionSubject { return Truth.assertAbout(SceneTransitionSubject.sceneTransitions()).that(transition) } @@ -74,14 +74,14 @@ private constructor( return actual as TransitionState.Idle } - fun isSceneTransition(): TransitionState.Transition.ChangeCurrentScene { - if (actual !is TransitionState.Transition.ChangeCurrentScene) { + fun isSceneTransition(): TransitionState.Transition.ChangeScene { + if (actual !is TransitionState.Transition.ChangeScene) { failWithActual( simpleFact("expected to be TransitionState.Transition.ChangeCurrentScene") ) } - return actual as TransitionState.Transition.ChangeCurrentScene + return actual as TransitionState.Transition.ChangeScene } fun isShowOrHideOverlayTransition(): TransitionState.Transition.ShowOrHideOverlay { @@ -183,8 +183,8 @@ abstract class BaseTransitionSubject<T : TransitionState.Transition>( class SceneTransitionSubject private constructor( metadata: FailureMetadata, - actual: TransitionState.Transition.ChangeCurrentScene, -) : BaseTransitionSubject<TransitionState.Transition.ChangeCurrentScene>(metadata, actual) { + actual: TransitionState.Transition.ChangeScene, +) : BaseTransitionSubject<TransitionState.Transition.ChangeScene>(metadata, actual) { fun hasFromScene(sceneKey: SceneKey) { check("fromScene").that(actual.fromScene).isEqualTo(sceneKey) } @@ -195,7 +195,7 @@ private constructor( companion object { fun sceneTransitions() = - Factory { metadata, actual: TransitionState.Transition.ChangeCurrentScene -> + Factory { metadata, actual: TransitionState.Transition.ChangeScene -> SceneTransitionSubject(metadata, actual) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt index baef620ad556..a36b0bca42b5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt @@ -218,4 +218,60 @@ class FaceHelpMessageDebouncerTest : SysuiTestCase() { assertThat(underTest.getMessageToShow(startWindow)?.msgId) .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE) } + + @Test + fun messageMustMeetThreshold() { + underTest = + FaceHelpMessageDebouncer( + window = window, + startWindow = 0, + shownFaceMessageFrequencyBoost = 0, + threshold = .8f, + ) + + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT, + "tooBright", + 0 + ) + ) + + // although tooClose message is the majority, it doesn't meet the 80% threshold + assertThat(underTest.getMessageToShow(startWindow)).isNull() + + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + + // message shows once it meets the threshold + assertThat(underTest.getMessageToShow(startWindow)?.msgId) + .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE) + assertThat(underTest.getMessageToShow(startWindow)?.msg).isEqualTo("tooClose") + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt index b31f6f5b096b..add7a7fbb8a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt @@ -16,11 +16,15 @@ package com.android.systemui.biometrics -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.keyguard.logging.BiometricMessageDeferralLogger +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.util.time.FakeSystemClock import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -31,14 +35,29 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @android.platform.test.annotations.EnabledOnRavenwood -class FaceHelpMessageDeferralTest : SysuiTestCase() { +class FaceHelpMessageDeferralTest(flags: FlagsParameterization) : SysuiTestCase() { val threshold = .75f @Mock lateinit var logger: BiometricMessageDeferralLogger @Mock lateinit var dumpManager: DumpManager + val systemClock = FakeSystemClock() + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } @Before fun setUp() { @@ -111,10 +130,11 @@ class FaceHelpMessageDeferralTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) fun testReturnsMostFrequentDeferredMessage() { val biometricMessageDeferral = createMsgDeferral(setOf(1, 2)) - // WHEN there's 80%of the messages are msgId=1 and 20% is msgId=2 + // WHEN there's 80% of the messages are msgId=1 and 20% is msgId=2 biometricMessageDeferral.processFrame(1) biometricMessageDeferral.processFrame(1) biometricMessageDeferral.processFrame(1) @@ -124,7 +144,41 @@ class FaceHelpMessageDeferralTest : SysuiTestCase() { biometricMessageDeferral.processFrame(2) biometricMessageDeferral.updateMessage(2, "msgId-2") - // THEN the most frequent deferred message is that meets the threshold is returned + // THEN the most frequent deferred message that meets the threshold is returned + assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage()) + } + + @Test + @EnableFlags(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) + fun testReturnsMostFrequentDeferredMessage_onlyAnalyzesLastNWindow() { + val biometricMessageDeferral = createMsgDeferral(setOf(1, 2)) + + // WHEN there's 80% of the messages are msgId=1 and 20% is msgId=2, but the last + // N window only contains messages with msgId=2 + repeat(80) { biometricMessageDeferral.processFrame(1) } + biometricMessageDeferral.updateMessage(1, "msgId-1") + systemClock.setElapsedRealtime(systemClock.elapsedRealtime() + 501L) + repeat(20) { biometricMessageDeferral.processFrame(2) } + biometricMessageDeferral.updateMessage(2, "msgId-2") + + // THEN the most frequent deferred message in the last N window (500L) is returned + assertEquals("msgId-2", biometricMessageDeferral.getDeferredMessage()) + } + + @Test + @DisableFlags(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) + fun testReturnsMostFrequentDeferredMessage_analyzesAllFrames() { + val biometricMessageDeferral = createMsgDeferral(setOf(1, 2)) + + // WHEN there's 80% of the messages are msgId=1 and 20% is msgId=2, but the last + // N window only contains messages with msgId=2 + repeat(80) { biometricMessageDeferral.processFrame(1) } + biometricMessageDeferral.updateMessage(1, "msgId-1") + systemClock.setElapsedRealtime(systemClock.elapsedRealtime() + 501L) + repeat(20) { biometricMessageDeferral.processFrame(2) } + biometricMessageDeferral.updateMessage(2, "msgId-2") + + // THEN the most frequent deferred message is returned assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage()) } @@ -213,14 +267,17 @@ class FaceHelpMessageDeferralTest : SysuiTestCase() { private fun createMsgDeferral( messagesToDefer: Set<Int>, acquiredInfoToIgnore: Set<Int> = emptySet(), + windowToAnalyzeLastNFrames: Long = 500L, ): BiometricMessageDeferral { return BiometricMessageDeferral( - messagesToDefer, - acquiredInfoToIgnore, - threshold, - logger, - dumpManager, - "0", + messagesToDefer = messagesToDefer, + acquiredInfoToIgnore = acquiredInfoToIgnore, + threshold = threshold, + windowToAnalyzeLastNFrames = windowToAnalyzeLastNFrames, + logBuffer = logger, + dumpManager = dumpManager, + id = "0", + systemClock = { systemClock }, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt index 76920e406bff..3b0057d87048 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt @@ -81,6 +81,7 @@ class CommunalDreamStartableTest : SysuiTestCase() { @Test fun startDreamWhenTransitioningToHub() = testScope.runTest { + keyguardRepository.setKeyguardShowing(true) keyguardRepository.setDreaming(false) powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON) whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt index d2515858dc0b..1a426d6ebce2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepositoryImplTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.communal.smartspace.CommunalSmartspaceController import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock @@ -67,6 +68,7 @@ class CommunalSmartspaceRepositoryImplTest : SysuiTestCase() { smartspaceController, fakeExecutor, systemClock, + logcatLogBuffer("CommunalSmartspaceRepositoryImplTest"), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 1d03ced19c72..99fcbb854e3a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -352,7 +352,7 @@ class CommunalInteractorTest : SysuiTestCase() { smartspaceRepository.setTimers(targets) - val smartspaceContent by collectLastValue(underTest.getOngoingContent(true)) + val smartspaceContent by collectLastValue(underTest.ongoingContent) assertThat(smartspaceContent?.size).isEqualTo(totalTargets) for (index in 0 until totalTargets) { assertThat(smartspaceContent?.get(index)?.size).isEqualTo(expectedSizes[index]) @@ -368,7 +368,7 @@ class CommunalInteractorTest : SysuiTestCase() { // Media is playing. mediaRepository.mediaActive() - val umoContent by collectLastValue(underTest.getOngoingContent(true)) + val umoContent by collectLastValue(underTest.ongoingContent) assertThat(umoContent?.size).isEqualTo(1) assertThat(umoContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java) @@ -376,20 +376,6 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun umo_mediaPlaying_doNotShowUmo() = - testScope.run { - // Tutorial completed. - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - - // Media is playing. - mediaRepository.mediaActive() - - val umoContent by collectLastValue(underTest.getOngoingContent(false)) - - assertThat(umoContent?.size).isEqualTo(0) - } - - @Test fun ongoing_shouldOrderAndSizeByTimestamp() = testScope.runTest { // Keyguard showing, and tutorial completed. @@ -412,7 +398,7 @@ class CommunalInteractorTest : SysuiTestCase() { val timer3 = smartspaceTimer("timer3", timestamp = 4L) smartspaceRepository.setTimers(listOf(timer1, timer2, timer3)) - val ongoingContent by collectLastValue(underTest.getOngoingContent(true)) + val ongoingContent by collectLastValue(underTest.ongoingContent) assertThat(ongoingContent?.size).isEqualTo(4) assertThat(ongoingContent?.get(0)?.key) .isEqualTo(CommunalContentModel.KEY.smartspace("timer3")) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 82181788e1be..179ba2256442 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -20,9 +20,7 @@ import android.appwidget.AppWidgetProviderInfo import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Intent -import android.content.pm.ActivityInfo import android.content.pm.PackageManager -import android.content.pm.ResolveInfo import android.content.pm.UserInfo import android.provider.Settings import android.view.accessibility.AccessibilityEvent @@ -72,7 +70,6 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.never @@ -141,6 +138,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { context, accessibilityManager, packageManager, + WIDGET_PICKER_PACKAGE_NAME, ) } @@ -259,18 +257,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Test fun onOpenWidgetPicker_launchesWidgetPickerActivity() { testScope.runTest { - whenever(packageManager.resolveActivity(any(), anyInt())).then { - ResolveInfo().apply { - activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } - } - } - val success = - underTest.onOpenWidgetPicker( - testableResources.resources, - packageManager, - activityResultLauncher - ) + underTest.onOpenWidgetPicker(testableResources.resources, activityResultLauncher) verify(activityResultLauncher).launch(any()) assertTrue(success) @@ -278,38 +266,14 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { } @Test - fun onOpenWidgetPicker_launcherActivityNotResolved_doesNotLaunchWidgetPickerActivity() { - testScope.runTest { - whenever(packageManager.resolveActivity(any(), anyInt())).thenReturn(null) - - val success = - underTest.onOpenWidgetPicker( - testableResources.resources, - packageManager, - activityResultLauncher - ) - - verify(activityResultLauncher, never()).launch(any()) - assertFalse(success) - } - } - - @Test fun onOpenWidgetPicker_activityLaunchThrowsException_failure() { testScope.runTest { - whenever(packageManager.resolveActivity(any(), anyInt())).then { - ResolveInfo().apply { - activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } - } - } - whenever(activityResultLauncher.launch(any())) .thenThrow(ActivityNotFoundException::class.java) val success = underTest.onOpenWidgetPicker( testableResources.resources, - packageManager, activityResultLauncher, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index f6f5bc038209..780d3576c5e4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -97,6 +97,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -757,7 +758,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // updateViewVisibility is called when the flow is collected. assertThat(communalContent).isNotNull() - verify(mediaHost).updateViewVisibility() + verify(mediaHost, atLeastOnce()).updateViewVisibility() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index eda9039c748e..d21a8270ae54 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.dreams +import android.app.WindowConfiguration import android.content.ComponentName import android.content.Intent import android.os.RemoteException @@ -65,6 +66,8 @@ import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.gesture.domain.gestureInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.navigationbar.gestural.domain.GestureInteractor +import com.android.systemui.navigationbar.gestural.domain.TaskInfo +import com.android.systemui.navigationbar.gestural.domain.TaskMatcher import com.android.systemui.testKosmos import com.android.systemui.touch.TouchInsetManager import com.android.systemui.util.concurrency.FakeExecutor @@ -83,13 +86,17 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.isNull +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq +import org.mockito.kotlin.firstValue import org.mockito.kotlin.mock import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.verifyZeroInteractions import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @@ -315,6 +322,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -332,6 +340,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -351,6 +360,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -373,6 +383,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -391,6 +402,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -406,6 +418,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -413,6 +426,47 @@ class DreamOverlayServiceTest : SysuiTestCase() { } @Test + fun testDeferredResetRespondsToAnimationEnd() { + val client = client + + // Inform the overlay service of dream starting. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*isPreview*/, + true /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + + whenever(mStateController.areExitAnimationsRunning()).thenReturn(true) + clearInvocations(mStateController, mTouchMonitor) + + // Starting a dream will cause it to end first. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*isPreview*/, + true /*shouldShowComplication*/ + ) + + mMainExecutor.runAllReady() + + verifyZeroInteractions(mTouchMonitor) + + val captor = ArgumentCaptor.forClass(DreamOverlayStateController.Callback::class.java) + verify(mStateController).addCallback(captor.capture()) + + whenever(mStateController.areExitAnimationsRunning()).thenReturn(false) + + captor.firstValue.onStateChanged() + + // Should only be called once since it should be null during the second reset. + verify(mTouchMonitor).destroy() + } + + @Test fun testLowLightSetByStartDream() { val client = client @@ -421,6 +475,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, LOW_LIGHT_COMPONENT.flattenToString(), + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -437,6 +492,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, HOME_CONTROL_PANEL_DREAM_COMPONENT.flattenToString(), + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -453,6 +509,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, LOW_LIGHT_COMPONENT.flattenToString(), + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -487,6 +544,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) // Immediately end the dream. @@ -518,6 +576,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -537,6 +596,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, LOW_LIGHT_COMPONENT.flattenToString(), + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -588,6 +648,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -611,6 +672,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -631,6 +693,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -660,6 +723,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -683,6 +747,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -708,6 +773,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -733,6 +799,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) // Set communal available, verify that overlay callback is informed. @@ -761,6 +828,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -781,6 +849,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -800,6 +869,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -823,6 +893,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -853,6 +924,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) testScope.runCurrent() @@ -869,6 +941,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -892,6 +965,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -923,6 +997,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -954,6 +1029,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -989,6 +1065,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() @@ -1015,7 +1092,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { } @Test - fun testDreamActivityGesturesBlockedOnStart() { + fun testDreamActivityGesturesBlockedWhenDreaming() { val client = client // Inform the overlay service of dream starting. @@ -1023,18 +1100,28 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - val captor = argumentCaptor<ComponentName>() + + val matcherCaptor = argumentCaptor<TaskMatcher>() verify(gestureInteractor) - .addGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global)) - assertThat(captor.firstValue.packageName) - .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName) + .addGestureBlockedMatcher(matcherCaptor.capture(), eq(GestureInteractor.Scope.Global)) + val matcher = matcherCaptor.firstValue + + val dreamTaskInfo = TaskInfo(mock<ComponentName>(), WindowConfiguration.ACTIVITY_TYPE_DREAM) + assertThat(matcher.matches(dreamTaskInfo)).isTrue() + + client.endDream() + mMainExecutor.runAllReady() + + verify(gestureInteractor) + .removeGestureBlockedMatcher(eq(matcher), eq(GestureInteractor.Scope.Global)) } @Test - fun testDreamActivityGesturesUnblockedOnEnd() { + fun testDreamActivityGesturesNotBlockedWhenPreview() { val client = client // Inform the overlay service of dream starting. @@ -1042,17 +1129,13 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + true /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - client.endDream() - mMainExecutor.runAllReady() - val captor = argumentCaptor<ComponentName>() - verify(gestureInteractor) - .removeGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global)) - assertThat(captor.firstValue.packageName) - .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName) + verify(gestureInteractor, never()) + .addGestureBlockedMatcher(any(), eq(GestureInteractor.Scope.Global)) } @Test @@ -1077,6 +1160,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, + false /*isPreview*/, false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt index 91d37cf1816a..a764256df9a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt @@ -16,12 +16,14 @@ package com.android.systemui.gesture.data +import android.app.WindowConfiguration import android.content.ComponentName import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl +import com.android.systemui.navigationbar.gestural.domain.TaskMatcher import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -40,14 +42,36 @@ class GestureRepositoryTest : SysuiTestCase() { @Test fun addRemoveComponentToBlock_updatesBlockedComponentSet() = testScope.runTest { - val component = mock<ComponentName>() + val matcher = TaskMatcher.TopActivityComponent(mock<ComponentName>()) - underTest.addGestureBlockedActivity(component) - val addedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities) - assertThat(addedBlockedComponents).contains(component) + kotlin.run { + underTest.addGestureBlockedMatcher(matcher) + val blockedMatchers by collectLastValue(underTest.gestureBlockedMatchers) + assertThat(blockedMatchers).contains(matcher) + } - underTest.removeGestureBlockedActivity(component) - val removedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities) - assertThat(removedBlockedComponents).isEmpty() + kotlin.run { + underTest.removeGestureBlockedMatcher(matcher) + val blockedMatchers by collectLastValue(underTest.gestureBlockedMatchers) + assertThat(blockedMatchers).doesNotContain(matcher) + } + } + + @Test + fun addRemoveActivityTypeToBlock_updatesBlockedActivityTypesSet() = + testScope.runTest { + val matcher = TaskMatcher.TopActivityType(WindowConfiguration.ACTIVITY_TYPE_STANDARD) + + kotlin.run { + underTest.addGestureBlockedMatcher(matcher) + val blockedMatchers by collectLastValue(underTest.gestureBlockedMatchers) + assertThat(blockedMatchers).contains(matcher) + } + + kotlin.run { + underTest.removeGestureBlockedMatcher(matcher) + val blockedMatchers by collectLastValue(underTest.gestureBlockedMatchers) + assertThat(blockedMatchers).doesNotContain(matcher) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt index 639544889c2a..0ce0d934c381 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.kosmos.backgroundCoroutineContext import com.android.systemui.kosmos.testDispatcher import com.android.systemui.navigationbar.gestural.data.gestureRepository import com.android.systemui.navigationbar.gestural.domain.GestureInteractor +import com.android.systemui.navigationbar.gestural.domain.TaskMatcher import com.android.systemui.shared.system.activityManagerWrapper import com.android.systemui.shared.system.taskStackChangeListeners import com.android.systemui.testKosmos @@ -76,13 +77,16 @@ class GestureInteractorTest : SysuiTestCase() { fun addBlockedActivity_testCombination() = testScope.runTest { val globalComponent = mock<ComponentName>() - repository.addGestureBlockedActivity(globalComponent) + repository.addGestureBlockedMatcher(TaskMatcher.TopActivityComponent(globalComponent)) val localComponent = mock<ComponentName>() val blocked by collectLastValue(underTest.topActivityBlocked) - underTest.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local) + underTest.addGestureBlockedMatcher( + TaskMatcher.TopActivityComponent(localComponent), + GestureInteractor.Scope.Local + ) assertThat(blocked).isFalse() @@ -95,7 +99,7 @@ class GestureInteractorTest : SysuiTestCase() { fun initialization_testEmit() = testScope.runTest { val globalComponent = mock<ComponentName>() - repository.addGestureBlockedActivity(globalComponent) + repository.addGestureBlockedMatcher(TaskMatcher.TopActivityComponent(globalComponent)) setTopActivity(globalComponent) val interactor = createInteractor() @@ -114,10 +118,36 @@ class GestureInteractorTest : SysuiTestCase() { val localComponent = mock<ComponentName>() - interactor1.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local) + interactor1.addGestureBlockedMatcher( + TaskMatcher.TopActivityComponent(localComponent), + GestureInteractor.Scope.Local + ) setTopActivity(localComponent) assertThat(interactor1Blocked).isTrue() assertThat(interactor2Blocked).isFalse() } + + @Test + fun matchingBlockers_separatelyManaged() = + testScope.runTest { + val interactor = createInteractor() + val interactorBlocked by collectLastValue(interactor.topActivityBlocked) + + val localComponent = mock<ComponentName>() + + val matcher1 = TaskMatcher.TopActivityComponent(localComponent) + val matcher2 = TaskMatcher.TopActivityComponent(localComponent) + + interactor.addGestureBlockedMatcher(matcher1, GestureInteractor.Scope.Local) + interactor.addGestureBlockedMatcher(matcher2, GestureInteractor.Scope.Local) + setTopActivity(localComponent) + assertThat(interactorBlocked).isTrue() + + interactor.removeGestureBlockedMatcher(matcher1, GestureInteractor.Scope.Local) + assertThat(interactorBlocked).isTrue() + + interactor.removeGestureBlockedMatcher(matcher2, GestureInteractor.Scope.Local) + assertThat(interactorBlocked).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/TaskMatcherTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/TaskMatcherTest.kt new file mode 100644 index 000000000000..a246270cf413 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/TaskMatcherTest.kt @@ -0,0 +1,91 @@ +/* + * 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.gesture.domain + +import android.app.WindowConfiguration +import android.content.ComponentName +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.navigationbar.gestural.domain.TaskInfo +import com.android.systemui.navigationbar.gestural.domain.TaskMatcher +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@SmallTest +class TaskMatcherTest : SysuiTestCase() { + @Test + fun activityMatcher_matchesComponentName() { + val componentName = ComponentName.unflattenFromString("com.foo/.bar")!! + val matcher = TaskMatcher.TopActivityComponent(componentName) + + val taskInfo = TaskInfo(componentName, WindowConfiguration.ACTIVITY_TYPE_STANDARD) + assertThat(matcher.matches(taskInfo)).isTrue() + } + + @Test + fun activityMatcher_doesNotMatchComponentName() { + val componentName = ComponentName.unflattenFromString("com.foo/.bar")!! + val matcher = TaskMatcher.TopActivityComponent(componentName) + + val taskInfo = + TaskInfo( + ComponentName.unflattenFromString("com.bar/.baz"), + WindowConfiguration.ACTIVITY_TYPE_STANDARD + ) + assertThat(matcher.matches(taskInfo)).isFalse() + } + + @Test + fun activityMatcher_matchesActivityType() { + val activityType = WindowConfiguration.ACTIVITY_TYPE_HOME + val matcher = TaskMatcher.TopActivityType(activityType) + + val taskInfo = TaskInfo(mock<ComponentName>(), activityType) + assertThat(matcher.matches(taskInfo)).isTrue() + } + + @Test + fun activityMatcher_doesNotMatchEmptyActivityType() { + val activityType = WindowConfiguration.ACTIVITY_TYPE_HOME + val matcher = TaskMatcher.TopActivityType(activityType) + + val taskInfo = TaskInfo(null, activityType) + assertThat(matcher.matches(taskInfo)).isFalse() + } + + @Test + fun activityMatcher_doesNotMatchActivityType() { + val activityType = WindowConfiguration.ACTIVITY_TYPE_HOME + val matcher = TaskMatcher.TopActivityType(activityType) + + val taskInfo = TaskInfo(mock<ComponentName>(), WindowConfiguration.ACTIVITY_TYPE_STANDARD) + assertThat(matcher.matches(taskInfo)).isFalse() + } + + @Test + fun activityMatcher_equivalentMatchersAreNotEqual() { + val activityType = WindowConfiguration.ACTIVITY_TYPE_HOME + val matcher1 = TaskMatcher.TopActivityType(activityType) + val matcher2 = TaskMatcher.TopActivityType(activityType) + + assertThat(matcher1).isNotEqualTo(matcher2) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt index 6eb9862fb4f1..9273dcef0de0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt @@ -39,12 +39,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.KeyguardState +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.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat @@ -53,6 +56,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor import com.android.systemui.testKosmos import junit.framework.Assert.assertEquals @@ -166,6 +170,10 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() = testScope.runTest { + val isGone by + collectLastValue( + kosmos.keyguardTransitionInteractor.isFinishedIn(Scenes.Gone, GONE) + ) powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( from = KeyguardState.AOD, @@ -175,7 +183,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { runCurrent() // Make sure we're GONE. - assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + assertEquals(true, isGone) // Get part way to AOD. powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) @@ -204,6 +212,10 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() = testScope.runTest { + val isGone by + collectLastValue( + kosmos.keyguardTransitionInteractor.isFinishedIn(Scenes.Gone, GONE) + ) powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( from = KeyguardState.AOD, @@ -213,7 +225,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { runCurrent() // Make sure we're GONE. - assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + assertEquals(true, isGone) // Get all the way to AOD powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) @@ -239,6 +251,10 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() = testScope.runTest { + val isLockscreen by + collectLastValue( + kosmos.keyguardTransitionInteractor.isFinishedIn(Scenes.Lockscreen, LOCKSCREEN) + ) powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( from = KeyguardState.AOD, @@ -248,10 +264,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { runCurrent() // Make sure we're in LOCKSCREEN. - assertEquals( - KeyguardState.LOCKSCREEN, - kosmos.keyguardTransitionInteractor.getFinishedState() - ) + assertEquals(true, isLockscreen) // Get part way to AOD. powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) @@ -327,6 +340,10 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetectedInAod_fromGone() = testScope.runTest { + val isGone by + collectLastValue( + kosmos.keyguardTransitionInteractor.isFinishedIn(Scenes.Gone, GONE) + ) powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( from = KeyguardState.AOD, @@ -336,7 +353,7 @@ class FromAodTransitionInteractorTest : SysuiTestCase() { runCurrent() // Make sure we're GONE. - assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + assertEquals(true, isGone) // Start going to AOD on first button push powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt index ee4a0d2d4e75..c18deb134075 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt @@ -48,12 +48,15 @@ import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.KeyguardState +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.TransitionState import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat import com.android.systemui.kosmos.applicationCoroutineScope @@ -62,6 +65,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import junit.framework.Assert.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -316,6 +320,10 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromGone() = testScope.runTest { + val isGone by + collectLastValue( + kosmos.keyguardTransitionInteractor.isFinishedIn(Scenes.Gone, GONE) + ) powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( from = KeyguardState.DOZING, @@ -325,7 +333,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT runCurrent() // Make sure we're GONE. - assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + assertEquals(true, isGone) // Get part way to AOD. powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) @@ -355,6 +363,10 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @Suppress("ktlint:standard:max-line-length") fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetectedAfterFinishedInAod_fromGone() = testScope.runTest { + val isGone by + collectLastValue( + kosmos.keyguardTransitionInteractor.isFinishedIn(Scenes.Gone, GONE) + ) powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( from = KeyguardState.DOZING, @@ -364,7 +376,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT runCurrent() // Make sure we're GONE. - assertEquals(KeyguardState.GONE, kosmos.keyguardTransitionInteractor.getFinishedState()) + assertEquals(true, isGone) // Get all the way to AOD powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) @@ -390,6 +402,10 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR) fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromLockscreen() = testScope.runTest { + val isLockscreen by + collectLastValue( + kosmos.keyguardTransitionInteractor.isFinishedIn(Scenes.Lockscreen, LOCKSCREEN) + ) powerInteractor.setAwakeForTest() transitionRepository.sendTransitionSteps( from = KeyguardState.DOZING, @@ -399,10 +415,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT runCurrent() // Make sure we're in LOCKSCREEN. - assertEquals( - KeyguardState.LOCKSCREEN, - kosmos.keyguardTransitionInteractor.getFinishedState() - ) + assertEquals(true, isLockscreen) // Get part way to AOD. powerInteractor.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_MIN) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 6e76cbce95f1..6708ffa2a091 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -90,52 +90,6 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun finishedKeyguardStateTests() = - testScope.runTest { - val finishedSteps by collectValues(underTest.finishedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) - runCurrent() - } - - assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD)) - } - - @Test - fun startedKeyguardStateTests() = - testScope.runTest { - val startedStates by collectValues(underTest.startedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) - runCurrent() - } - - assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE)) - } - - @Test fun startedKeyguardTransitionStepTests() = testScope.runTest { val startedSteps by collectValues(underTest.startedKeyguardTransitionStep) @@ -1206,95 +1160,6 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = - testScope.runTest { - val finishedStates by collectValues(underTest.finishedKeyguardState) - - // We default FINISHED in LOCKSCREEN. - assertEquals(listOf(LOCKSCREEN), finishedStates) - - sendSteps( - TransitionStep(LOCKSCREEN, AOD, 0f, STARTED), - TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING), - TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED), - ) - - // We're FINISHED in AOD. - assertEquals( - listOf( - LOCKSCREEN, - AOD, - ), - finishedStates - ) - - // Transition back to LOCKSCREEN. - sendSteps( - TransitionStep(AOD, LOCKSCREEN, 0f, STARTED), - TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING), - TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED), - ) - - // We're FINISHED in LOCKSCREEN. - assertEquals( - listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), - finishedStates - ) - - sendSteps( - TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), - TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), - ) - - // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in - // LOCKSCREEN. - assertEquals( - listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), - finishedStates - ) - - sendSteps( - TransitionStep(LOCKSCREEN, GONE, 0.6f, CANCELED), - ) - - // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN. - assertEquals( - listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), - finishedStates - ) - - sendSteps( - TransitionStep(GONE, LOCKSCREEN, 0.6f, STARTED), - TransitionStep(GONE, LOCKSCREEN, 0.9f, RUNNING), - TransitionStep(GONE, LOCKSCREEN, 1f, FINISHED), - ) - - // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to - // LOCKSCREEN after the cancellation. - assertEquals( - listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - LOCKSCREEN, - ), - finishedStates - ) - } - - @Test fun testCurrentState() = testScope.runTest { val currentStates by collectValues(underTest.currentKeyguardState) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt index 3e0a1f31302d..073ed61a949b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt @@ -19,18 +19,20 @@ package com.android.systemui.keyguard.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.fakeLightRevealScrimRepository +import com.android.systemui.keyguard.data.repository.DEFAULT_REVEAL_EFFECT import com.android.systemui.keyguard.data.repository.FakeLightRevealScrimRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.testKosmos import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Test @@ -64,35 +66,45 @@ class LightRevealScrimInteractorTest : SysuiTestCase() { @Test fun lightRevealEffect_doesNotChangeDuringKeyguardTransition() = - runTest(UnconfinedTestDispatcher()) { - val values = mutableListOf<LightRevealEffect>() - val job = underTest.lightRevealEffect.onEach(values::add).launchIn(this) + kosmos.testScope.runTest { + val values by collectValues(underTest.lightRevealEffect) + runCurrent() + assertEquals(listOf(DEFAULT_REVEAL_EFFECT), values) fakeLightRevealScrimRepository.setRevealEffect(reveal1) - + runCurrent() // The reveal effect shouldn't emit anything until a keyguard transition starts. - assertEquals(values.size, 0) + assertEquals(listOf(DEFAULT_REVEAL_EFFECT), values) // Once it starts, it should emit reveal1. fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(transitionState = TransitionState.STARTED) + TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED) ) - assertEquals(values, listOf(reveal1)) + runCurrent() + assertEquals(listOf(DEFAULT_REVEAL_EFFECT, reveal1), values) // Until the next transition starts, reveal2 should not be emitted. fakeLightRevealScrimRepository.setRevealEffect(reveal2) + runCurrent() fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(transitionState = TransitionState.RUNNING) + TransitionStep( + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.RUNNING + ) ) + runCurrent() fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(transitionState = TransitionState.FINISHED) + TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.FINISHED) ) - assertEquals(values, listOf(reveal1)) + runCurrent() + assertEquals(listOf(DEFAULT_REVEAL_EFFECT, reveal1), values) fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(transitionState = TransitionState.STARTED) + TransitionStep( + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED + ) ) - assertEquals(values, listOf(reveal1, reveal2)) - - job.cancel() + runCurrent() + assertEquals(listOf(DEFAULT_REVEAL_EFFECT, reveal1, reveal2), values) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt index 8c9c527bd6fd..ec6045ca0a68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt @@ -48,12 +48,13 @@ class HydratorTest : SysuiTestCase() { composeRule.setContent { val keepAlive by keepAliveMutable if (keepAlive) { - val viewModel = rememberViewModel { - FakeSysUiViewModel( - upstreamFlow = upstreamFlow, - upstreamStateFlow = upstreamStateFlow, - ) - } + val viewModel = + rememberViewModel("test") { + FakeSysUiViewModel( + upstreamFlow = upstreamFlow, + upstreamStateFlow = upstreamStateFlow, + ) + } Column { Text( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index 768fbca90b56..7203b61ecc9f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.composefragment.viewmodel +import android.app.StatusBarManager import android.content.testableContext import android.testing.TestableLooper.RunWithLooper import androidx.lifecycle.Lifecycle @@ -32,6 +33,8 @@ import com.android.systemui.qs.fgsManagerController import com.android.systemui.res.R import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.sysuiStatusBarStateController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -178,6 +181,23 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { } } + @Test + fun qsEnabled_followsRepository() = + with(kosmos) { + testScope.testWithinLifecycle { + val qsEnabled by collectLastValue(underTest.qsEnabled) + + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(disable2 = QS_DISABLE_FLAG) + + assertThat(qsEnabled).isFalse() + + fakeDisableFlagsRepository.disableFlags.value = DisableFlagsModel() + + assertThat(qsEnabled).isTrue() + } + } + private inline fun TestScope.testWithinLifecycle( crossinline block: suspend TestScope.() -> TestResult ): TestResult { @@ -186,4 +206,8 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() { block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) } } } + + companion object { + private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 4d3909c06efc..f365afbfcc06 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -501,7 +501,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun getCurrentSceneInUi(): SceneKey { return when (val state = transitionState.value) { is ObservableTransitionState.Idle -> state.currentScene - is ObservableTransitionState.Transition.ChangeCurrentScene -> state.fromScene + is ObservableTransitionState.Transition.ChangeScene -> state.fromScene is ObservableTransitionState.Transition.ShowOrHideOverlay -> state.currentScene is ObservableTransitionState.Transition.ReplaceOverlay -> state.currentScene } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 8f8d2e22e161..d3b51d1d17f7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -51,7 +51,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.scenetransition.lockscreenSceneTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus @@ -551,8 +551,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchToAOD_whenAvailable_whenDeviceSleepsLocked() = testScope.runTest { kosmos.lockscreenSceneTransitionInteractor.start() - val asleepState by - collectLastValue(kosmos.keyguardTransitionInteractor.asleepKeyguardState) + val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState) val currentTransitionInfo by collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal) val transitionState = @@ -584,8 +583,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchToDozing_whenAodUnavailable_whenDeviceSleepsLocked() = testScope.runTest { kosmos.lockscreenSceneTransitionInteractor.start() - val asleepState by - collectLastValue(kosmos.keyguardTransitionInteractor.asleepKeyguardState) + val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState) val currentTransitionInfo by collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal) val transitionState = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 733cac99f4ec..3f97f0b7a67d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -42,10 +42,14 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.BurnInModel -import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER 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.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -56,6 +60,10 @@ import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.res.R +import com.android.systemui.scene.data.repository.Idle +import com.android.systemui.scene.data.repository.Transition +import com.android.systemui.scene.data.repository.setTransition +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.mockLargeScreenHeaderHelper import com.android.systemui.shade.shadeTestUtil @@ -66,6 +74,7 @@ import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat 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 @@ -295,34 +304,47 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // Start transitioning to glanceable hub val progress = 0.6f - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GLANCEABLE_HUB, - value = 0f, - ) + kosmos.setTransition( + sceneTransition = Transition(from = Scenes.Lockscreen, to = Scenes.Communal), + stateTransition = + TransitionStep( + transitionState = TransitionState.STARTED, + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + value = 0f, + ) ) + runCurrent() - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.RUNNING, - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GLANCEABLE_HUB, - value = progress, - ) + kosmos.setTransition( + sceneTransition = + Transition( + from = Scenes.Lockscreen, + to = Scenes.Communal, + progress = flowOf(progress) + ), + stateTransition = + TransitionStep( + transitionState = TransitionState.RUNNING, + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + value = progress, + ) ) + runCurrent() assertThat(alpha).isIn(Range.closed(0f, 1f)) // Finish transition to glanceable hub - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.FINISHED, - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GLANCEABLE_HUB, - value = 1f, - ) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Communal), + stateTransition = + TransitionStep( + transitionState = TransitionState.FINISHED, + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + value = 1f, + ) ) assertThat(alpha).isEqualTo(0f) @@ -348,35 +370,46 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // Start transitioning to glanceable hub val progress = 0.6f - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.DREAMING, - to = KeyguardState.GLANCEABLE_HUB, - value = 0f, - ) + kosmos.setTransition( + sceneTransition = Transition(from = Scenes.Lockscreen, to = Scenes.Communal), + stateTransition = + TransitionStep( + transitionState = TransitionState.STARTED, + from = DREAMING, + to = GLANCEABLE_HUB, + value = 0f, + ) ) runCurrent() - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.RUNNING, - from = KeyguardState.DREAMING, - to = KeyguardState.GLANCEABLE_HUB, - value = progress, - ) + kosmos.setTransition( + sceneTransition = + Transition( + from = Scenes.Lockscreen, + to = Scenes.Communal, + progress = flowOf(progress) + ), + stateTransition = + TransitionStep( + transitionState = TransitionState.RUNNING, + from = DREAMING, + to = GLANCEABLE_HUB, + value = progress, + ) ) runCurrent() // Keep notifications hidden during the transition from dream to hub assertThat(alpha).isEqualTo(0) // Finish transition to glanceable hub - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.FINISHED, - from = KeyguardState.DREAMING, - to = KeyguardState.GLANCEABLE_HUB, - value = 1f, - ) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Communal), + stateTransition = + TransitionStep( + transitionState = TransitionState.FINISHED, + from = DREAMING, + to = GLANCEABLE_HUB, + value = 1f, + ) ) assertThat(alpha).isEqualTo(0f) } @@ -400,35 +433,47 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S testScope.runTest { val isOnLockscreen by collectLastValue(underTest.isOnLockscreen) - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - testScope, + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(from = LOCKSCREEN, to = GONE) ) assertThat(isOnLockscreen).isFalse() + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(from = GONE, to = LOCKSCREEN) + ) + assertThat(isOnLockscreen).isTrue() // While progressing from lockscreen, should still be true - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, - value = 0.8f, - transitionState = TransitionState.RUNNING - ) + kosmos.setTransition( + sceneTransition = Transition(from = Scenes.Lockscreen, to = Scenes.Gone), + stateTransition = + TransitionStep( + from = LOCKSCREEN, + to = GONE, + value = 0.8f, + transitionState = TransitionState.RUNNING + ) ) assertThat(isOnLockscreen).isTrue() - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GONE, - to = KeyguardState.LOCKSCREEN, - testScope, + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = + TransitionStep( + from = GONE, + to = LOCKSCREEN, + ) ) assertThat(isOnLockscreen).isTrue() - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.PRIMARY_BOUNCER, - testScope, + kosmos.setTransition( + sceneTransition = Idle(Scenes.Bouncer), + stateTransition = + TransitionStep( + from = LOCKSCREEN, + to = PRIMARY_BOUNCER, + ) ) assertThat(isOnLockscreen).isTrue() } @@ -442,8 +487,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S shadeTestUtil.setLockscreenShadeExpansion(0f) shadeTestUtil.setQsExpansion(0f) keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.OCCLUDED, + from = LOCKSCREEN, + to = OCCLUDED, testScope, ) assertThat(isOnLockscreenWithoutShade).isFalse() @@ -480,11 +525,15 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S assertThat(isOnGlanceableHubWithoutShade).isFalse() // Move to glanceable hub - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GLANCEABLE_HUB, - testScope = this + kosmos.setTransition( + sceneTransition = Idle(Scenes.Communal), + stateTransition = + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + ) ) + assertThat(isOnGlanceableHubWithoutShade).isTrue() // While state is GLANCEABLE_HUB, validate variations of both shade and qs expansion @@ -502,6 +551,14 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S shadeTestUtil.setQsExpansion(0f) shadeTestUtil.setLockscreenShadeExpansion(0f) + kosmos.setTransition( + sceneTransition = Idle(Scenes.Communal), + stateTransition = + TransitionStep( + from = LOCKSCREEN, + to = GLANCEABLE_HUB, + ) + ) assertThat(isOnGlanceableHubWithoutShade).isTrue() } @@ -808,8 +865,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // GONE transition gets to 90% complete keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, + from = LOCKSCREEN, + to = GONE, transitionState = TransitionState.STARTED, value = 0f, ) @@ -817,8 +874,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S runCurrent() keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, + from = LOCKSCREEN, + to = GONE, transitionState = TransitionState.RUNNING, value = 0.9f, ) @@ -843,8 +900,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // OCCLUDED transition gets to 90% complete keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.OCCLUDED, + from = LOCKSCREEN, + to = OCCLUDED, transitionState = TransitionState.STARTED, value = 0f, ) @@ -852,8 +909,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S runCurrent() keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.OCCLUDED, + from = LOCKSCREEN, + to = OCCLUDED, transitionState = TransitionState.RUNNING, value = 0.9f, ) @@ -877,8 +934,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S showLockscreen() keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.GONE, + from = LOCKSCREEN, + to = GONE, testScope ) keyguardRepository.setStatusBarState(StatusBarState.SHADE) @@ -922,8 +979,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // ... then user hits power to go to AOD keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, + from = LOCKSCREEN, + to = AOD, testScope, ) // ... followed by a shade collapse @@ -945,7 +1002,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // PRIMARY_BOUNCER->GONE transition is started keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.STARTED, value = 0f, @@ -956,7 +1013,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // PRIMARY_BOUNCER->GONE transition running keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.1f, @@ -967,7 +1024,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.9f, @@ -979,7 +1036,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S hideCommunalScene() keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.FINISHED, value = 1f @@ -1003,7 +1060,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // PRIMARY_BOUNCER->GONE transition is started keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.STARTED, ) @@ -1013,7 +1070,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // PRIMARY_BOUNCER->GONE transition running keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.1f, @@ -1024,7 +1081,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.9f, @@ -1035,7 +1092,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.PRIMARY_BOUNCER, + from = PRIMARY_BOUNCER, to = GONE, transitionState = TransitionState.FINISHED, value = 1f @@ -1058,7 +1115,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // ALTERNATE_BOUNCER->GONE transition is started keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.STARTED, value = 0f, @@ -1069,7 +1126,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // ALTERNATE_BOUNCER->GONE transition running keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.1f, @@ -1080,7 +1137,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.9f, @@ -1092,7 +1149,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S hideCommunalScene() keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.FINISHED, value = 1f @@ -1116,7 +1173,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // ALTERNATE_BOUNCER->GONE transition is started keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.STARTED, ) @@ -1126,7 +1183,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S // ALTERNATE_BOUNCER->GONE transition running keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.1f, @@ -1137,7 +1194,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.RUNNING, value = 0.9f, @@ -1148,7 +1205,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.ALTERNATE_BOUNCER, + from = ALTERNATE_BOUNCER, to = GONE, transitionState = TransitionState.FINISHED, value = 1f @@ -1165,8 +1222,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) runCurrent() keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, + from = AOD, + to = LOCKSCREEN, testScope, ) } @@ -1178,8 +1235,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardRepository.setDreaming(true) runCurrent() keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.DREAMING, + from = LOCKSCREEN, + to = DREAMING, testScope, ) } @@ -1191,8 +1248,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED) runCurrent() keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, + from = AOD, + to = LOCKSCREEN, testScope, ) } @@ -1204,8 +1261,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED) runCurrent() keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, + from = AOD, + to = LOCKSCREEN, testScope, ) } @@ -1219,8 +1276,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S kosmos.keyguardBouncerRepository.setPrimaryShow(true) runCurrent() keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GLANCEABLE_HUB, - to = KeyguardState.PRIMARY_BOUNCER, + from = GLANCEABLE_HUB, + to = PRIMARY_BOUNCER, testScope, ) } @@ -1234,8 +1291,8 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S kosmos.keyguardBouncerRepository.setPrimaryShow(false) runCurrent() keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.GLANCEABLE_HUB, - to = KeyguardState.ALTERNATE_BOUNCER, + from = GLANCEABLE_HUB, + to = ALTERNATE_BOUNCER, testScope, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt index 51a70bda6034..fe6c7417032f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R import com.android.systemui.statusbar.policy.configurationController import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.testKosmos @@ -157,6 +158,10 @@ class VolumePanelViewModelTest : SysuiTestCase() { @Test fun testDumpingState() = test({ + testableResources.addOverride(R.bool.volume_panel_is_large_screen, false) + testableResources.overrideConfiguration( + Configuration().apply { orientation = Configuration.ORIENTATION_PORTRAIT } + ) componentByKey = mapOf( COMPONENT_1 to mockVolumePanelUiComponentProvider, diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 823ff9f54be3..e8fd2ef6eafa 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -727,6 +727,10 @@ .75 </item> + <!-- The last x ms of face acquired info messages to analyze to determine + whether to show a deferred face auth help message. --> + <integer name="config_face_help_msgs_defer_analyze_timeframe">500</integer> + <!-- Which face help messages to surface when fingerprint is also enrolled. Message ids correspond with the acquired ids in BiometricFaceConstants --> <integer-array name="config_face_help_msgs_when_fingerprint_enrolled"> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index fd943d0a5414..e6cc6cf766c6 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -740,6 +740,8 @@ <string name="quick_settings_bluetooth_device_connected">Connected</string> <!-- QuickSettings: Bluetooth dialog device in audio sharing default summary [CHAR LIMIT=50]--> <string name="quick_settings_bluetooth_device_audio_sharing">Audio Sharing</string> + <!-- QuickSettings: Bluetooth dialog device summary for devices that are capable of audio sharing and switching to active[CHAR LIMIT=NONE]--> + <string name="quick_settings_bluetooth_device_audio_sharing_or_switch_active">Tap to switch or share audio</string> <!-- QuickSettings: Bluetooth dialog device saved default summary [CHAR LIMIT=NONE]--> <string name="quick_settings_bluetooth_device_saved">Saved</string> <!-- QuickSettings: Accessibility label to disconnect a device [CHAR LIMIT=NONE]--> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java index 64fe78d9958b..7ec977a8d6aa 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java @@ -218,6 +218,7 @@ public class Task { @ViewDebug.ExportedProperty(category="recents") public String title; @ViewDebug.ExportedProperty(category="recents") + @Nullable public String titleDescription; @ViewDebug.ExportedProperty(category="recents") public int colorPrimary; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt index 1685f49e4f3e..4731ebba7124 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt @@ -25,11 +25,13 @@ import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatu * - startWindow: Window of time on start required before showing the first help message * - shownFaceMessageFrequencyBoost: Frequency boost given to messages that are currently shown to * the user + * - threshold: minimum percentage of frames a message must appear in order to show it */ class FaceHelpMessageDebouncer( private val window: Long = DEFAULT_WINDOW_MS, private val startWindow: Long = window, private val shownFaceMessageFrequencyBoost: Int = 4, + private val threshold: Float = 0f, ) { private val TAG = "FaceHelpMessageDebouncer" private var startTime = 0L @@ -56,7 +58,7 @@ class FaceHelpMessageDebouncer( } } - private fun getMostFrequentHelpMessage(): HelpFaceAuthenticationStatus? { + private fun getMostFrequentHelpMessageSurpassingThreshold(): HelpFaceAuthenticationStatus? { // freqMap: msgId => frequency val freqMap = helpFaceAuthStatuses.groupingBy { it.msgId }.eachCount().toMutableMap() @@ -83,7 +85,25 @@ class FaceHelpMessageDebouncer( } } ?.key - return helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency } + + if (msgIdWithHighestFrequency == null) { + return null + } + + val freq = + if (msgIdWithHighestFrequency == lastMessageIdShown) { + freqMap[msgIdWithHighestFrequency]!! - shownFaceMessageFrequencyBoost + } else { + freqMap[msgIdWithHighestFrequency]!! + } + .toFloat() + + return if ((freq / helpFaceAuthStatuses.size.toFloat()) >= threshold) { + helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency } + } else { + Log.v(TAG, "most frequent helpFaceAuthStatus didn't make the threshold: $threshold") + null + } } fun addMessage(helpFaceAuthStatus: HelpFaceAuthenticationStatus) { @@ -98,14 +118,15 @@ class FaceHelpMessageDebouncer( return null } removeOldMessages(atTimestamp) - val messageToShow = getMostFrequentHelpMessage() + val messageToShow = getMostFrequentHelpMessageSurpassingThreshold() if (lastMessageIdShown != messageToShow?.msgId) { Log.v( TAG, "showMessage previousLastMessageId=$lastMessageIdShown" + "\n\tmessageToShow=$messageToShow " + "\n\thelpFaceAuthStatusesSize=${helpFaceAuthStatuses.size}" + - "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses" + "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses" + + "\n\tthreshold=$threshold" ) lastMessageIdShown = messageToShow?.msgId } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt index 90d06fb0bec1..d382adaff955 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt @@ -17,14 +17,19 @@ package com.android.systemui.biometrics import android.content.res.Resources +import android.os.SystemClock.elapsedRealtime import com.android.keyguard.logging.BiometricMessageDeferralLogger import com.android.systemui.Dumpable +import com.android.systemui.Flags.faceMessageDeferUpdate import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.BiometricLog import com.android.systemui.res.R +import com.android.systemui.util.time.SystemClock +import dagger.Lazy import java.io.PrintWriter import java.util.Objects import java.util.UUID @@ -36,7 +41,8 @@ class FaceHelpMessageDeferralFactory constructor( @Main private val resources: Resources, @BiometricLog private val logBuffer: LogBuffer, - private val dumpManager: DumpManager + private val dumpManager: DumpManager, + private val systemClock: Lazy<SystemClock>, ) { fun create(): FaceHelpMessageDeferral { val id = UUID.randomUUID().toString() @@ -45,6 +51,7 @@ constructor( logBuffer = BiometricMessageDeferralLogger(logBuffer, "FaceHelpMessageDeferral[$id]"), dumpManager = dumpManager, id = id, + systemClock, ) } } @@ -58,14 +65,17 @@ class FaceHelpMessageDeferral( logBuffer: BiometricMessageDeferralLogger, dumpManager: DumpManager, val id: String, + val systemClock: Lazy<SystemClock>, ) : BiometricMessageDeferral( resources.getIntArray(R.array.config_face_help_msgs_defer_until_timeout).toHashSet(), resources.getIntArray(R.array.config_face_help_msgs_ignore).toHashSet(), resources.getFloat(R.dimen.config_face_help_msgs_defer_until_timeout_threshold), + resources.getInteger(R.integer.config_face_help_msgs_defer_analyze_timeframe).toLong(), logBuffer, dumpManager, id, + systemClock, ) /** @@ -77,10 +87,24 @@ open class BiometricMessageDeferral( private val messagesToDefer: Set<Int>, private val acquiredInfoToIgnore: Set<Int>, private val threshold: Float, + private val windowToAnalyzeLastNFrames: Long, private val logBuffer: BiometricMessageDeferralLogger, dumpManager: DumpManager, id: String, + private val systemClock: Lazy<SystemClock>, ) : Dumpable { + + private val faceHelpMessageDebouncer: FaceHelpMessageDebouncer? = + if (faceMessageDeferUpdate()) { + FaceHelpMessageDebouncer( + window = windowToAnalyzeLastNFrames, + startWindow = 0L, + shownFaceMessageFrequencyBoost = 0, + threshold = threshold, + ) + } else { + null + } private val acquiredInfoToFrequency: MutableMap<Int, Int> = HashMap() private val acquiredInfoToHelpString: MutableMap<Int, String> = HashMap() private var mostFrequentAcquiredInfoToDefer: Int? = null @@ -97,13 +121,20 @@ open class BiometricMessageDeferral( pw.println("messagesToDefer=$messagesToDefer") pw.println("totalFrames=$totalFrames") pw.println("threshold=$threshold") + pw.println("faceMessageDeferUpdateFlagEnabled=${faceMessageDeferUpdate()}") + if (faceMessageDeferUpdate()) { + pw.println("windowToAnalyzeLastNFrames(ms)=$windowToAnalyzeLastNFrames") + } } /** Reset all saved counts. */ fun reset() { totalFrames = 0 - mostFrequentAcquiredInfoToDefer = null - acquiredInfoToFrequency.clear() + if (!faceMessageDeferUpdate()) { + mostFrequentAcquiredInfoToDefer = null + acquiredInfoToFrequency.clear() + } + acquiredInfoToHelpString.clear() logBuffer.reset() } @@ -137,24 +168,48 @@ open class BiometricMessageDeferral( logBuffer.logFrameIgnored(acquiredInfo) return } - totalFrames++ - val newAcquiredInfoCount = acquiredInfoToFrequency.getOrDefault(acquiredInfo, 0) + 1 - acquiredInfoToFrequency[acquiredInfo] = newAcquiredInfoCount - if ( - messagesToDefer.contains(acquiredInfo) && - (mostFrequentAcquiredInfoToDefer == null || - newAcquiredInfoCount > - acquiredInfoToFrequency.getOrDefault(mostFrequentAcquiredInfoToDefer!!, 0)) - ) { - mostFrequentAcquiredInfoToDefer = acquiredInfo + if (faceMessageDeferUpdate()) { + faceHelpMessageDebouncer?.let { + val helpFaceAuthStatus = + HelpFaceAuthenticationStatus( + msgId = acquiredInfo, + msg = null, + systemClock.get().elapsedRealtime() + ) + if (totalFrames == 1) { // first frame + it.startNewFaceAuthSession(helpFaceAuthStatus.createdAt) + } + it.addMessage(helpFaceAuthStatus) + } + } else { + val newAcquiredInfoCount = acquiredInfoToFrequency.getOrDefault(acquiredInfo, 0) + 1 + acquiredInfoToFrequency[acquiredInfo] = newAcquiredInfoCount + if ( + messagesToDefer.contains(acquiredInfo) && + (mostFrequentAcquiredInfoToDefer == null || + newAcquiredInfoCount > + acquiredInfoToFrequency.getOrDefault( + mostFrequentAcquiredInfoToDefer!!, + 0 + )) + ) { + mostFrequentAcquiredInfoToDefer = acquiredInfo + } } logBuffer.logFrameProcessed( acquiredInfo, totalFrames, - mostFrequentAcquiredInfoToDefer?.toString() + if (faceMessageDeferUpdate()) { + faceHelpMessageDebouncer + ?.getMessageToShow(systemClock.get().elapsedRealtime()) + ?.msgId + .toString() + } else { + mostFrequentAcquiredInfoToDefer?.toString() + } ) } @@ -166,9 +221,16 @@ open class BiometricMessageDeferral( * [threshold] percentage. */ fun getDeferredMessage(): CharSequence? { - mostFrequentAcquiredInfoToDefer?.let { - if (acquiredInfoToFrequency.getOrDefault(it, 0) > (threshold * totalFrames)) { - return acquiredInfoToHelpString[it] + if (faceMessageDeferUpdate()) { + faceHelpMessageDebouncer?.let { + val helpFaceAuthStatus = it.getMessageToShow(systemClock.get().elapsedRealtime()) + return acquiredInfoToHelpString[helpFaceAuthStatus?.msgId] + } + } else { + mostFrequentAcquiredInfoToDefer?.let { + if (acquiredInfoToFrequency.getOrDefault(it, 0) > (threshold * totalFrames)) { + return acquiredInfoToHelpString[it] + } } } return null diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index cd9b9bc71f32..0b440ad81fb5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -45,6 +45,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory +import com.android.systemui.Flags.bpIconA11y import com.android.systemui.biometrics.Utils.ellipsize import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality @@ -54,6 +55,7 @@ import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode import com.android.systemui.biometrics.ui.viewmodel.PromptMessage import com.android.systemui.biometrics.ui.viewmodel.PromptSize import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel +import com.android.systemui.common.ui.view.onTouchListener import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper @@ -330,17 +332,31 @@ object BiometricViewBinder { // reuse the icon as a confirm button launch { - viewModel.isIconConfirmButton - .map { isPending -> - when { - isPending && modalities.hasFaceAndFingerprint -> - View.OnTouchListener { _: View, event: MotionEvent -> - viewModel.onOverlayTouch(event) - } - else -> null + if (bpIconA11y()) { + viewModel.isIconConfirmButton.collect { isButton -> + if (isButton) { + iconView.onTouchListener { _: View, event: MotionEvent -> + viewModel.onOverlayTouch(event) + } + iconView.setOnClickListener { viewModel.confirmAuthenticated() } + } else { + iconView.setOnTouchListener(null) + iconView.setOnClickListener(null) } } - .collect { onTouch -> iconView.setOnTouchListener(onTouch) } + } else { + viewModel.isIconConfirmButton + .map { isPending -> + when { + isPending && modalities.hasFaceAndFingerprint -> + View.OnTouchListener { _: View, event: MotionEvent -> + viewModel.onOverlayTouch(event) + } + else -> null + } + } + .collect { onTouch -> iconView.setOnTouchListener(onTouch) } + } } // dismiss prompt when authenticated and confirmed @@ -358,7 +374,8 @@ object BiometricViewBinder { // Allow icon to be used as confirmation button with udfps and a11y // enabled if ( - accessibilityManager.isTouchExplorationEnabled && + !bpIconA11y() && + accessibilityManager.isTouchExplorationEnabled && modalities.hasUdfps ) { iconView.setOnClickListener { viewModel.confirmAuthenticated() } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index 4a358c0b1292..bdd4c161ad59 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -31,6 +31,8 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500), @UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507), @UiEvent(doc = "Audio sharing device clicked, do nothing") AUDIO_SHARING_DEVICE_CLICKED(1699), + @UiEvent(doc = "Available audio sharing device clicked, do nothing") + AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED(1880), @UiEvent(doc = "Connected other device clicked to disconnect") CONNECTED_OTHER_DEVICE_DISCONNECT(1508), @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617), @@ -44,8 +46,14 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent doc = "Not broadcasting, having one connected, another saved LE audio device is clicked" ) LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719), + @Deprecated( + "Use case no longer needed", + ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED") + ) @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked") - LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720); + LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720), + @UiEvent(doc = "Not broadcasting, having two connected, the active LE audio devices is clicked") + LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt index a78130f1b041..2ba4c73a0293 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt @@ -38,6 +38,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice enum class DeviceItemType { ACTIVE_MEDIA_BLUETOOTH_DEVICE, AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, AVAILABLE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE, SAVED_BLUETOOTH_DEVICE, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index 9d82e7677a87..f1894d3bb111 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -67,7 +67,7 @@ constructor( backgroundDispatcher, logger ), - NotSharingClickedConnected( + NotSharingClickedActive( leAudioProfile, assistantProfile, backgroundDispatcher, @@ -106,6 +106,12 @@ constructor( DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED) } + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + // TODO(b/360759048): pop up dialog + uiEventLogger.log( + BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED + ) + } DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { setActive() uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE) @@ -238,14 +244,14 @@ constructor( BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED } - private class NotSharingClickedConnected( + private class NotSharingClickedActive( private val leAudioProfile: LeAudioProfile?, private val assistantProfile: LocalBluetoothLeBroadcastAssistant?, @Background private val backgroundDispatcher: CoroutineDispatcher, private val logger: BluetoothTileDialogLogger, ) : LaunchSettingsCriteria { - // If not broadcasting, having two device connected, clicked on any connected LE audio - // devices + // If not broadcasting, having two device connected, clicked on the active LE audio + // device override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { return withContext(backgroundDispatcher) { val matched = @@ -259,7 +265,7 @@ constructor( logger ) .size == 2 && - deviceItem.isActiveOrConnectedLeAudioSupported + deviceItem.isActiveLeAudioSupported } } ?: false @@ -275,7 +281,7 @@ constructor( } override suspend fun getClickUiEvent(deviceItem: DeviceItem) = - BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED + BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED } private companion object { @@ -290,10 +296,8 @@ constructor( val DeviceItem.isNotConnectedLeAudioSupported: Boolean get() = type == DeviceItemType.SAVED_BLUETOOTH_DEVICE && isLeAudioSupported - val DeviceItem.isActiveOrConnectedLeAudioSupported: Boolean - get() = - (type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE || - type == DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) && isLeAudioSupported + val DeviceItem.isActiveLeAudioSupported: Boolean + get() = type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE && isLeAudioSupported val DeviceItem.isMediaDevice: Boolean get() = diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index e846bf7b523c..7280489e0835 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -125,6 +125,37 @@ internal class AudioSharingMediaDeviceItemFactory( } } +internal class AvailableAudioSharingMediaDeviceItemFactory( + private val localBluetoothManager: LocalBluetoothManager? +) : AvailableMediaDeviceItemFactory() { + override fun isFilterMatched( + context: Context, + cachedDevice: CachedBluetoothDevice, + audioManager: AudioManager + ): Boolean { + return BluetoothUtils.isAudioSharingEnabled() && + super.isFilterMatched(context, cachedDevice, audioManager) && + BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice( + cachedDevice, + localBluetoothManager + ) + } + + override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { + return createDeviceItem( + context, + cachedDevice, + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + context.getString( + R.string.quick_settings_bluetooth_device_audio_sharing_or_switch_active + ), + if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, + "", + isActive = false + ) + } +} + internal class ActiveHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() { override fun isFilterMatched( context: Context, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt index 95244964dc44..9114ecac7ac7 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt @@ -118,6 +118,7 @@ constructor( listOf( ActiveMediaDeviceItemFactory(), AudioSharingMediaDeviceItemFactory(localBluetoothManager), + AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager), AvailableMediaDeviceItemFactory(), ConnectedDeviceItemFactory(), SavedDeviceItemFactory() @@ -127,6 +128,7 @@ constructor( listOf( DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, DeviceItemType.SAVED_BLUETOOTH_DEVICE, 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 c1f7d590d08e..102ae7abb3e2 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 @@ -52,7 +52,9 @@ object ComposeBouncerViewBinder { setContent { PlatformTheme { BouncerContent( - rememberViewModel { viewModelFactory.create() }, + rememberViewModel("ComposeBouncerViewBinder") { + viewModelFactory.create() + }, dialogFactory, ) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index abca518745d1..c67b35424cc9 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -22,7 +22,6 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationResul import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -40,7 +39,10 @@ sealed class AuthMethodBouncerViewModel( * being able to attempt to unlock the device. */ val isInputEnabled: StateFlow<Boolean>, -) : SysUiViewModel, ExclusiveActivatable() { + + /** Name to use for performance tracing purposes. */ + val traceName: String, +) : ExclusiveActivatable() { private val _animateFailure = MutableStateFlow(false) /** 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 d21eccdfb047..05b46564c5ba 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 @@ -39,7 +39,6 @@ import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.util.kotlin.Utils.Companion.sample @@ -80,7 +79,7 @@ constructor( private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor, private val flags: ComposeBouncerFlags, -) : SysUiViewModel, ExclusiveActivatable() { +) : ExclusiveActivatable() { /** * A message shown when the user has attempted the wrong credential too many times and now must * wait a while before attempting to authenticate again. 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 79e5f8d4a683..b985fc4efece 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 @@ -23,6 +23,7 @@ import android.graphics.Bitmap import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.type import androidx.core.graphics.drawable.toBitmap +import com.android.app.tracing.coroutines.traceCoroutine import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationWipeModel @@ -34,7 +35,6 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -63,7 +63,7 @@ constructor( private val pinViewModelFactory: PinBouncerViewModel.Factory, private val patternViewModelFactory: PatternBouncerViewModel.Factory, private val passwordViewModelFactory: PasswordBouncerViewModel.Factory, -) : SysUiViewModel, ExclusiveActivatable() { +) : ExclusiveActivatable() { private val _selectedUserImage = MutableStateFlow<Bitmap?>(null) val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow() @@ -147,7 +147,7 @@ constructor( .map(::getChildViewModel) .collectLatest { childViewModelOrNull -> _authMethodViewModel.value = childViewModelOrNull - childViewModelOrNull?.activate() + childViewModelOrNull?.let { traceCoroutine(it.traceName) { it.activate() } } } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index c91fd6a1a18e..fc860e53aac5 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -53,6 +53,7 @@ constructor( AuthMethodBouncerViewModel( interactor = interactor, isInputEnabled = isInputEnabled, + traceName = "PasswordBouncerViewModel", ) { private val _password = MutableStateFlow("") diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt index 4c029299e16b..60ec3019d09d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -51,6 +51,7 @@ constructor( AuthMethodBouncerViewModel( interactor = interactor, isInputEnabled = isInputEnabled, + traceName = "PatternBouncerViewModel", ) { /** The number of columns in the dot grid. */ diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index c6119544d2b0..db78a98d96af 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -63,6 +63,7 @@ constructor( AuthMethodBouncerViewModel( interactor = interactor, isInputEnabled = isInputEnabled, + traceName = "PinBouncerViewModel", ) { /** * Whether the sim-related UI in the pin view is showing. diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt index 0582cc20927c..c69cea4a6a5a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt @@ -32,11 +32,14 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.filterState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach /** @@ -54,6 +57,18 @@ constructor( private val dreamManager: DreamManager, @Background private val bgScope: CoroutineScope, ) : CoreStartable { + /** Flow that emits when the dream should be started underneath the glanceable hub. */ + val startDream = + allOf( + keyguardTransitionInteractor + .transitionValue(Scenes.Communal, KeyguardState.GLANCEABLE_HUB) + .map { it == 1f }, + not(keyguardInteractor.isDreaming), + // TODO(b/362830856): Remove this workaround. + keyguardInteractor.isKeyguardShowing, + ) + .filter { it } + @SuppressLint("MissingPermission") override fun start() { if (!communalSettingsInteractor.isCommunalFlagEnabled()) { @@ -72,17 +87,10 @@ constructor( // Restart the dream underneath the hub in order to support the ability to swipe // away the hub to enter the dream. - keyguardTransitionInteractor - .transition( - edge = Edge.create(to = Scenes.Communal), - edgeWithoutSceneContainer = Edge.create(to = KeyguardState.GLANCEABLE_HUB) - ) - .filterState(TransitionState.FINISHED) + startDream .sampleFilter(powerInteractor.isAwake) { isAwake -> - dreamManager.canStartDreaming(isAwake) + !glanceableHubAllowKeyguardWhenDreaming() && dreamManager.canStartDreaming(isAwake) } - .sampleFilter(keyguardInteractor.isDreaming) { isDreaming -> !isDreaming } - .filter { !glanceableHubAllowKeyguardWhenDreaming() } .onEach { dreamManager.startDream() } .launchIn(bgScope) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index ba2b7bf96a30..a33e0ac0b33a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.dagger import android.content.Context +import android.content.res.Resources import com.android.systemui.CoreStartable import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalDatabaseModule @@ -38,6 +39,8 @@ import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.EditWidgetsActivityStarterImpl import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSource import com.android.systemui.scene.shared.model.SceneDataSourceDelegator @@ -90,6 +93,7 @@ interface CommunalModule { companion object { const val LOGGABLE_PREFIXES = "loggable_prefixes" + const val LAUNCHER_PACKAGE = "launcher_package" @Provides @Communal @@ -126,5 +130,12 @@ interface CommunalModule { .getStringArray(com.android.internal.R.array.config_loggable_dream_prefixes) .toList() } + + /** The package name of the launcher */ + @Provides + @Named(LAUNCHER_PACKAGE) + fun provideLauncherPackage(@Main resources: Resources): String { + return resources.getString(R.string.launcher_overlayable_package) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt index 86241a5261d7..f77dd587dca3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSmartspaceRepository.kt @@ -24,6 +24,9 @@ import com.android.systemui.communal.data.model.CommunalSmartspaceTimer import com.android.systemui.communal.smartspace.CommunalSmartspaceController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.util.time.SystemClock import java.util.concurrent.Executor @@ -49,8 +52,11 @@ constructor( private val communalSmartspaceController: CommunalSmartspaceController, @Main private val uiExecutor: Executor, private val systemClock: SystemClock, + @CommunalLog logBuffer: LogBuffer, ) : CommunalSmartspaceRepository, BcSmartspaceDataPlugin.SmartspaceTargetListener { + private val logger = Logger(logBuffer, "CommunalSmartspaceRepository") + private val _timers: MutableStateFlow<List<CommunalSmartspaceTimer>> = MutableStateFlow(emptyList()) override val timers: Flow<List<CommunalSmartspaceTimer>> = _timers @@ -87,6 +93,8 @@ constructor( remoteViews = target.remoteViews!!, ) } + + logger.d({ "Smartspace timers updated: $str1" }) { str1 = _timers.value.toString() } } override fun startListening() { diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 98abbebd1951..9b96341bdd8e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -510,7 +510,7 @@ constructor( * A flow of ongoing content, including smartspace timers and umo, ordered by creation time and * sized dynamically. */ - fun getOngoingContent(mediaHostVisible: Boolean): Flow<List<CommunalContentModel.Ongoing>> = + val ongoingContent: Flow<List<CommunalContentModel.Ongoing>> = combine(smartspaceRepository.timers, mediaRepository.mediaModel) { timers, media -> val ongoingContent = mutableListOf<CommunalContentModel.Ongoing>() @@ -526,7 +526,7 @@ constructor( ) // Add UMO - if (mediaHostVisible && media.hasAnyMediaOrRecommendation) { + if (media.hasAnyMediaOrRecommendation) { ongoingContent.add( CommunalContentModel.Umo( createdTimestampMillis = media.createdTimestampMillis, diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt index 5bbb46d21d8b..e04d3095d68d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt @@ -182,6 +182,7 @@ constructor( } private suspend fun finishCurrentTransition() { + if (currentTransitionId == null) return internalTransitionInteractor.updateTransition( currentTransitionId!!, 1f, @@ -224,7 +225,7 @@ constructor( collectProgress(transition) } else if (transition.toScene == CommunalScenes.Communal) { if (currentToState == KeyguardState.GLANCEABLE_HUB) { - transitionKtfTo(transitionInteractor.getStartedFromState()) + transitionKtfTo(transitionInteractor.startedKeyguardTransitionStep.value.from) } startTransitionToGlanceableHub() collectProgress(transition) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 16788d15b269..65f0679c4266 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -29,6 +29,7 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityManager import androidx.activity.result.ActivityResultLauncher import com.android.internal.logging.UiEventLogger +import com.android.systemui.communal.dagger.CommunalModule.Companion.LAUNCHER_PACKAGE import com.android.systemui.communal.data.model.CommunalWidgetCategories import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor @@ -81,6 +82,7 @@ constructor( @Application private val context: Context, private val accessibilityManager: AccessibilityManager, private val packageManager: PackageManager, + @Named(LAUNCHER_PACKAGE) private val launcherPackage: String, ) : BaseCommunalViewModel(communalSceneInteractor, communalInteractor, mediaHost) { private val logger = Logger(logBuffer, "CommunalEditModeViewModel") @@ -185,7 +187,6 @@ constructor( /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */ suspend fun onOpenWidgetPicker( resources: Resources, - packageManager: PackageManager, activityLauncher: ActivityResultLauncher<Intent> ): Boolean = withContext(backgroundDispatcher) { @@ -196,7 +197,7 @@ constructor( ) { it.providerInfo } - getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let { + getWidgetPickerActivityIntent(resources, excludeList)?.let { try { activityLauncher.launch(it) return@withContext true @@ -209,18 +210,10 @@ constructor( private fun getWidgetPickerActivityIntent( resources: Resources, - packageManager: PackageManager, excludeList: ArrayList<AppWidgetProviderInfo> ): Intent? { - val packageName = - getLauncherPackageName(packageManager) - ?: run { - Log.e(TAG, "Couldn't resolve launcher package name") - return@getWidgetPickerActivityIntent null - } - return Intent(Intent.ACTION_PICK).apply { - setPackage(packageName) + setPackage(launcherPackage) putExtra( EXTRA_DESIRED_WIDGET_WIDTH, resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width) @@ -247,16 +240,6 @@ constructor( } } - private fun getLauncherPackageName(packageManager: PackageManager): String? { - return packageManager - .resolveActivity( - Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }, - PackageManager.MATCH_DEFAULT_ONLY - ) - ?.activityInfo - ?.packageName - } - /** Sets whether edit mode is currently open */ fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 5a39a6272c94..d69ba1b23aa3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -99,7 +99,7 @@ constructor( private val logger = Logger(logBuffer, "CommunalViewModel") - private val _isMediaHostVisible = + private val isMediaHostVisible = conflatedCallbackFlow { val callback = { visible: Boolean -> trySend(visible) @@ -117,12 +117,26 @@ constructor( mediaHost.updateViewVisibility() emit(mediaHost.visible) } + .distinctUntilChanged() .onEach { logger.d({ "_isMediaHostVisible: $bool1" }) { bool1 = it } } .flowOn(mainDispatcher) /** Communal content saved from the previous emission when the flow is active (not "frozen"). */ private var frozenCommunalContent: List<CommunalContentModel>? = null + private val ongoingContent = + combine( + isMediaHostVisible, + communalInteractor.ongoingContent.onEach { mediaHost.updateViewVisibility() } + ) { mediaVisible, ongoingContent -> + if (mediaVisible) { + ongoingContent + } else { + // Media is not visible, don't show UMO + ongoingContent.filterNot { it is CommunalContentModel.Umo } + } + } + @OptIn(ExperimentalCoroutinesApi::class) private val latestCommunalContent: Flow<List<CommunalContentModel>> = tutorialInteractor.isTutorialAvailable @@ -130,8 +144,6 @@ constructor( if (isTutorialMode) { return@flatMapLatest flowOf(communalInteractor.tutorialContent) } - val ongoingContent = - _isMediaHostVisible.flatMapLatest { communalInteractor.getOngoingContent(it) } combine( ongoingContent, communalInteractor.widgetContent, @@ -254,6 +266,7 @@ constructor( expandedMatchesParentHeight = true showsOnlyActiveMedia = false falsingProtectionNeeded = false + disablePagination = true init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 55a24d0f595a..d84dc209196e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -270,11 +270,7 @@ constructor( private fun onOpenWidgetPicker() { lifecycleScope.launch { - communalViewModel.onOpenWidgetPicker( - resources, - packageManager, - addWidgetActivityLauncher - ) + communalViewModel.onOpenWidgetPicker(resources, addWidgetActivityLauncher) } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index caf5b01db846..e3f740e6ff72 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -24,12 +24,11 @@ import static com.android.systemui.dreams.dagger.DreamModule.DREAM_TOUCH_INSET_M import static com.android.systemui.dreams.dagger.DreamModule.HOME_CONTROL_PANEL_DREAM_COMPONENT; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import android.app.WindowConfiguration; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; import android.graphics.drawable.ColorDrawable; -import android.service.dreams.DreamActivity; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -65,6 +64,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.navigationbar.gestural.domain.GestureInteractor; +import com.android.systemui.navigationbar.gestural.domain.TaskMatcher; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -89,6 +89,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ LifecycleOwner { private static final String TAG = "DreamOverlayService"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final TaskMatcher DREAM_TYPE_MATCHER = + new TaskMatcher.TopActivityType(WindowConfiguration.ACTIVITY_TYPE_DREAM); // The Context is used to construct the hosting constraint layout and child overlay views. private final Context mContext; @@ -141,10 +143,6 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final TouchInsetManager mTouchInsetManager; private final LifecycleOwner mLifecycleOwner; - - - private ComponentName mCurrentBlockedGestureDreamActivityComponent; - private final ArrayList<Job> mFlows = new ArrayList<>(); /** @@ -221,16 +219,122 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } }; - private final DreamOverlayStateController.Callback mExitAnimationFinishedCallback = - new DreamOverlayStateController.Callback() { - @Override - public void onStateChanged() { - if (!mStateController.areExitAnimationsRunning()) { - mStateController.removeCallback(mExitAnimationFinishedCallback); - resetCurrentDreamOverlayLocked(); + /** + * {@link ResetHandler} protects resetting {@link DreamOverlayService} by making sure reset + * requests are processed before subsequent actions proceed. Requests themselves are also + * ordered between each other as well to ensure actions are correctly sequenced. + */ + private final class ResetHandler { + @FunctionalInterface + interface Callback { + void onComplete(); + } + + private record Info(Callback callback, String source) {} + + private final ArrayList<Info> mPendingCallbacks = new ArrayList<>(); + + DreamOverlayStateController.Callback mStateCallback = + new DreamOverlayStateController.Callback() { + @Override + public void onStateChanged() { + process(true); } + }; + + /** + * Called from places where there is no need to wait for the reset to complete. This still + * will defer the reset until it is okay to reset and also sequences the request with + * others. + */ + public void reset(String source) { + reset(()-> {}, source); + } + + /** + * Invoked to request a reset with a callback that will fire after reset if it is deferred. + * + * @return {@code true} if the reset happened immediately, {@code false} if it was deferred + * and will fire later, invoking the callback. + */ + public boolean reset(Callback callback, String source) { + // Always add listener pre-emptively + if (mPendingCallbacks.isEmpty()) { + mStateController.addCallback(mStateCallback); + } + + final Info info = new Info(callback, source); + mPendingCallbacks.add(info); + process(false); + + boolean processed = !mPendingCallbacks.contains(info); + + if (!processed) { + Log.d(TAG, "delayed resetting from: " + source); + } + + return processed; + } + + private void resetInternal() { + // This ensures the container view of the current dream is removed before + // the controller is potentially reset. + removeContainerViewFromParentLocked(); + + if (mStarted && mWindow != null) { + try { + mWindow.clearContentView(); + mWindowManager.removeView(mWindow.getDecorView()); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error removing decor view when resetting overlay", e); } - }; + } + + mStateController.setOverlayActive(false); + mStateController.setLowLightActive(false); + mStateController.setEntryAnimationsFinished(false); + + if (mDreamOverlayContainerViewController != null) { + mDreamOverlayContainerViewController.destroy(); + mDreamOverlayContainerViewController = null; + } + + if (mTouchMonitor != null) { + mTouchMonitor.destroy(); + mTouchMonitor = null; + } + + mWindow = null; + + // Always unregister the any set DreamActivity from being blocked from gestures. + mGestureInteractor.removeGestureBlockedMatcher(DREAM_TYPE_MATCHER, + GestureInteractor.Scope.Global); + + mStarted = false; + } + + private boolean canReset() { + return !mStateController.areExitAnimationsRunning(); + } + + private void process(boolean fromDelayedCallback) { + while (canReset() && !mPendingCallbacks.isEmpty()) { + final Info callbackInfo = mPendingCallbacks.removeFirst(); + resetInternal(); + callbackInfo.callback.onComplete(); + + if (fromDelayedCallback) { + Log.d(TAG, "reset overlay (delayed) for " + callbackInfo.source); + } + } + + if (mPendingCallbacks.isEmpty()) { + mStateController.removeCallback(mStateCallback); + } + } + } + + private final ResetHandler mResetHandler = new ResetHandler(); private final DreamOverlayStateController mStateController; @@ -344,10 +448,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mExecutor.execute(() -> { setLifecycleStateLocked(Lifecycle.State.DESTROYED); - - resetCurrentDreamOverlayLocked(); - mDestroyed = true; + mResetHandler.reset("destroying"); }); mDispatcher.onServicePreSuperOnDestroy(); @@ -387,7 +489,10 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // Reset the current dream overlay before starting a new one. This can happen // when two dreams overlap (briefly, for a smoother dream transition) and both // dreams are bound to the dream overlay service. - resetCurrentDreamOverlayLocked(); + if (!mResetHandler.reset(() -> onStartDream(layoutParams), + "starting with dream already started")) { + return; + } } mDreamOverlayContainerViewController = @@ -399,7 +504,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // If we are not able to add the overlay window, reset the overlay. if (!addOverlayWindowLocked(layoutParams)) { - resetCurrentDreamOverlayLocked(); + mResetHandler.reset("couldn't add window while starting"); return; } @@ -420,7 +525,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStarted = true; updateRedirectWakeup(); - updateBlockedGestureDreamActivityComponent(); + + if (!isDreamInPreviewMode()) { + mGestureInteractor.addGestureBlockedMatcher(DREAM_TYPE_MATCHER, + GestureInteractor.Scope.Global); + } } private void updateRedirectWakeup() { @@ -431,21 +540,9 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ redirectWake(mCommunalAvailable && !glanceableHubAllowKeyguardWhenDreaming()); } - private void updateBlockedGestureDreamActivityComponent() { - // TODO(b/343815446): We should not be crafting this ActivityInfo ourselves. It should be - // in a common place, Such as DreamActivity itself. - final ActivityInfo info = new ActivityInfo(); - info.name = DreamActivity.class.getName(); - info.packageName = getDreamComponent().getPackageName(); - mCurrentBlockedGestureDreamActivityComponent = info.getComponentName(); - - mGestureInteractor.addGestureBlockedActivity(mCurrentBlockedGestureDreamActivityComponent, - GestureInteractor.Scope.Global); - } - @Override public void onEndDream() { - resetCurrentDreamOverlayLocked(); + mResetHandler.reset("ending dream"); } @Override @@ -576,49 +673,4 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ Log.w(TAG, "Removing dream overlay container view parent!"); parentView.removeView(containerView); } - - private void resetCurrentDreamOverlayLocked() { - if (mStateController.areExitAnimationsRunning()) { - mStateController.addCallback(mExitAnimationFinishedCallback); - return; - } - - // This ensures the container view of the current dream is removed before - // the controller is potentially reset. - removeContainerViewFromParentLocked(); - - if (mStarted && mWindow != null) { - try { - mWindow.clearContentView(); - mWindowManager.removeView(mWindow.getDecorView()); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Error removing decor view when resetting overlay", e); - } - } - - mStateController.setOverlayActive(false); - mStateController.setLowLightActive(false); - mStateController.setEntryAnimationsFinished(false); - - if (mDreamOverlayContainerViewController != null) { - mDreamOverlayContainerViewController.destroy(); - mDreamOverlayContainerViewController = null; - } - - if (mTouchMonitor != null) { - mTouchMonitor.destroy(); - mTouchMonitor = null; - } - - mWindow = null; - - // Always unregister the any set DreamActivity from being blocked from gestures. - if (mCurrentBlockedGestureDreamActivityComponent != null) { - mGestureInteractor.removeGestureBlockedActivity( - mCurrentBlockedGestureDreamActivityComponent, GestureInteractor.Scope.Global); - mCurrentBlockedGestureDreamActivityComponent = null; - } - - mStarted = false; - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index b6543074cdef..a20dfa5a4c3e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -25,7 +25,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.data.repository.InputDeviceRepository import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceAdded -import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceChange import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.DeviceRemoved import com.android.systemui.inputdevice.data.repository.InputDeviceRepository.FreshStart import com.android.systemui.keyboard.data.model.Keyboard @@ -78,24 +77,16 @@ constructor( inputDeviceRepository: InputDeviceRepository ) : KeyboardRepository { - private val keyboardsChange: Flow<Pair<Collection<Int>, DeviceChange>> = - inputDeviceRepository.deviceChange - .map { (ids, change) -> ids.filter { id -> isPhysicalFullKeyboard(id) } to change } - .filter { (_, change) -> - when (change) { - FreshStart -> true - is DeviceAdded -> isPhysicalFullKeyboard(change.deviceId) - is DeviceRemoved -> isPhysicalFullKeyboard(change.deviceId) - } - } - @FlowPreview override val newlyConnectedKeyboard: Flow<Keyboard> = - keyboardsChange + inputDeviceRepository.deviceChange .flatMapConcat { (devices, operation) -> when (operation) { - FreshStart -> devices.asFlow() - is DeviceAdded -> flowOf(operation.deviceId) + FreshStart -> devices.filter { id -> isPhysicalFullKeyboard(id) }.asFlow() + is DeviceAdded -> { + if (isPhysicalFullKeyboard(operation.deviceId)) flowOf(operation.deviceId) + else emptyFlow() + } is DeviceRemoved -> emptyFlow() } } @@ -103,8 +94,8 @@ constructor( .flowOn(backgroundDispatcher) override val isAnyKeyboardConnected: Flow<Boolean> = - keyboardsChange - .map { (devices, _) -> devices.isNotEmpty() } + inputDeviceRepository.deviceChange + .map { (ids, _) -> ids.any { id -> isPhysicalFullKeyboard(id) } } .distinctUntilChanged() .flowOn(backgroundDispatcher) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 6e04133dcb4a..4cf9ec8890d4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -89,7 +89,7 @@ constructor( .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() } .debounce(50L) .sample( - startedKeyguardTransitionStep, + transitionInteractor.startedKeyguardTransitionStep, wakeToGoneInteractor.canWakeDirectlyToGone, ) .collect { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 4666430398ec..2434b29c0cdd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -117,7 +117,7 @@ constructor( if (SceneContainerFlag.isEnabled) return scope.launch { keyguardInteractor.primaryBouncerShowing - .sample(startedKeyguardTransitionStep, ::Pair) + .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair) .collect { pair -> val (isBouncerShowing, lastStartedTransitionStep) = pair if ( @@ -132,7 +132,7 @@ constructor( fun startToLockscreenOrGlanceableHubTransition(openHub: Boolean) { scope.launch { if ( - transitionInteractor.startedKeyguardState.replayCache.last() == + transitionInteractor.startedKeyguardTransitionStep.value.to == KeyguardState.DREAMING ) { if (powerInteractor.detailedWakefulness.value.isAwake()) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index cd3df07eea55..228e01edc99b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -134,16 +134,12 @@ constructor( .filterRelevantKeyguardState() .sampleCombine( internalTransitionInteractor.currentTransitionInfoInternal, - finishedKeyguardState, + transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN), keyguardInteractor.isActiveDreamLockscreenHosted, ) .collect { - ( - isAbleToDream, - transitionInfo, - finishedKeyguardState, - isActiveDreamLockscreenHosted) -> - val isOnLockscreen = finishedKeyguardState == KeyguardState.LOCKSCREEN + (isAbleToDream, transitionInfo, isOnLockscreen, isActiveDreamLockscreenHosted) + -> val isTransitionInterruptible = transitionInfo.to == KeyguardState.LOCKSCREEN && !invalidFromStates.contains(transitionInfo.from) @@ -189,7 +185,7 @@ constructor( scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") { shadeRepository.legacyShadeExpansion .sampleCombine( - startedKeyguardTransitionStep, + transitionInteractor.startedKeyguardTransitionStep, internalTransitionInteractor.currentTransitionInfoInternal, keyguardInteractor.statusBarState, keyguardInteractor.isKeyguardDismissible, @@ -334,7 +330,7 @@ constructor( listenForSleepTransition( modeOnCanceledFromStartedStep = { startedStep -> if ( - transitionInteractor.asleepKeyguardState.value == KeyguardState.AOD && + keyguardInteractor.asleepKeyguardState.value == KeyguardState.AOD && startedStep.from == KeyguardState.AOD ) { TransitionModeOnCanceled.REVERSE diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt index f9ab1bbcc741..bde0f56aa691 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt @@ -62,16 +62,16 @@ constructor( communalInteractor .transitionProgressToScene(toScene) .sample( - transitionInteractor.startedKeyguardState, + transitionInteractor.startedKeyguardTransitionStep, ::Pair, ) - .collect { (transitionProgress, lastStartedState) -> + .collect { (transitionProgress, lastStartedStep) -> val id = transitionId if (id == null) { // No transition started. if ( transitionProgress is CommunalTransitionProgressModel.Transition && - lastStartedState == fromState + lastStartedStep.to == fromState ) { transitionId = transitionRepository.startTransition( @@ -84,7 +84,7 @@ constructor( ) } } else { - if (lastStartedState != toState) { + if (lastStartedStep.to != toState) { return@collect } // An existing `id` means a transition is started, and calls to diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index a96d7a8f0997..f6f0cc58be71 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -36,7 +36,9 @@ import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.DozeTransitionModel 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.DOZING 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 @@ -409,6 +411,12 @@ constructor( } } + /** Which keyguard state to use when the device goes to sleep. */ + val asleepKeyguardState: StateFlow<KeyguardState> = + repository.isAodAvailable + .map { aodAvailable -> if (aodAvailable) AOD else DOZING } + .stateIn(applicationScope, SharingStarted.Eagerly, DOZING) + /** * Whether the primary authentication is required for the given user due to lockdown or * encryption after reboot. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt index 4a8ada7f1184..505c749d9e44 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt @@ -48,7 +48,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -74,9 +73,7 @@ constructor( val isLongPressHandlingEnabled: StateFlow<Boolean> = if (isFeatureEnabled()) { combine( - transitionInteractor.finishedKeyguardState.map { - it == KeyguardState.LOCKSCREEN - }, + transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN), repository.isQuickSettingsVisible, ) { isFullyTransitionedToLockScreen, isQuickSettingsVisible -> isFullyTransitionedToLockScreen && !isQuickSettingsVisible diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 6ff369ec8711..92e2a911317f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -22,14 +22,16 @@ import android.util.Log import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +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.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED import com.android.systemui.keyguard.shared.model.TransitionState @@ -47,7 +49,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -56,7 +57,6 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch /** Encapsulates business-logic related to the keyguard transitions. */ @@ -66,7 +66,6 @@ class KeyguardTransitionInteractor @Inject constructor( @Application val scope: CoroutineScope, - private val keyguardRepository: KeyguardRepository, private val repository: KeyguardTransitionRepository, private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>, private val fromPrimaryBouncerTransitionInteractor: @@ -126,8 +125,10 @@ constructor( repository.transitions .filter { it.transitionState != TransitionState.CANCELED } .collect { step -> - getTransitionValueFlow(step.from).emit(1f - step.value) - getTransitionValueFlow(step.to).emit(step.value) + val value = + if (step.transitionState == TransitionState.FINISHED) 1f else step.value + getTransitionValueFlow(step.from).emit(1f - value) + getTransitionValueFlow(step.to).emit(value) } } @@ -183,8 +184,14 @@ constructor( } } - fun transition(edge: Edge, edgeWithoutSceneContainer: Edge): Flow<TransitionStep> { - return transition(if (SceneContainerFlag.isEnabled) edge else edgeWithoutSceneContainer) + fun transition(edge: Edge, edgeWithoutSceneContainer: Edge? = null): Flow<TransitionStep> { + return transition( + if (SceneContainerFlag.isEnabled || edgeWithoutSceneContainer == null) { + edge + } else { + edgeWithoutSceneContainer + } + ) } /** Given an [edge], return a Flow to collect only relevant [TransitionStep]s. */ @@ -250,10 +257,10 @@ constructor( } fun transitionValue( - scene: SceneKey, + scene: SceneKey? = null, stateWithoutSceneContainer: KeyguardState, ): Flow<Float> { - return if (SceneContainerFlag.isEnabled) { + return if (SceneContainerFlag.isEnabled && scene != null) { sceneInteractor.transitionProgress(scene) } else { transitionValue(stateWithoutSceneContainer) @@ -277,73 +284,10 @@ constructor( } /** The last [TransitionStep] with a [TransitionState] of STARTED */ - val startedKeyguardTransitionStep: Flow<TransitionStep> = - repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED } - - /** The destination state of the last [TransitionState.STARTED] transition. */ - @SuppressLint("SharedFlowCreation") - val startedKeyguardState: SharedFlow<KeyguardState> = - startedKeyguardTransitionStep - .map { step -> step.to } - .buffer(2, BufferOverflow.DROP_OLDEST) - .shareIn(scope, SharingStarted.Eagerly, replay = 1) - - /** The from state of the last [TransitionState.STARTED] transition. */ - // TODO: is it performant to have several SharedFlows side by side instead of one? - @SuppressLint("SharedFlowCreation") - val startedKeyguardFromState: SharedFlow<KeyguardState> = - startedKeyguardTransitionStep - .map { step -> step.from } - .buffer(2, BufferOverflow.DROP_OLDEST) - .shareIn(scope, SharingStarted.Eagerly, replay = 1) - - /** Which keyguard state to use when the device goes to sleep. */ - val asleepKeyguardState: StateFlow<KeyguardState> = - keyguardRepository.isAodAvailable - .map { aodAvailable -> if (aodAvailable) AOD else DOZING } - .stateIn(scope, SharingStarted.Eagerly, DOZING) - - /** - * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition. - * - * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a - * value when a subsequent transition is STARTED. It will *only* emit once we have finally - * FINISHED in a state. This can have unintuitive implications. - * - * For example, if we're transitioning from GONE -> DOZING, and that transition is CANCELED in - * favor of a DOZING -> LOCKSCREEN transition, the FINISHED state is still GONE, and will remain - * GONE throughout the DOZING -> LOCKSCREEN transition until the DOZING -> LOCKSCREEN transition - * finishes (at which point we'll be FINISHED in LOCKSCREEN). - * - * Since there's no real limit to how many consecutive transitions can be canceled, it's even - * possible for the FINISHED state to be the same as the STARTED state while still - * transitioning. - * - * For example: - * 1. We're finished in GONE. - * 2. The user presses the power button, starting a GONE -> DOZING transition. We're still - * FINISHED in GONE. - * 3. The user changes their mind, pressing the power button to wake up; this starts a DOZING -> - * LOCKSCREEN transition. We're still FINISHED in GONE. - * 4. The user quickly swipes away the lockscreen prior to DOZING -> LOCKSCREEN finishing; this - * starts a LOCKSCREEN -> GONE transition. We're still FINISHED in GONE, but we've also - * STARTED a transition *to* GONE. - * 5. We'll emit KeyguardState.GONE again once the transition finishes. - * - * If you just need to know when we eventually settle into a state, this flow is likely - * sufficient. However, if you're having issues with state *during* transitions started after - * one or more canceled transitions, you probably need to use [currentKeyguardState]. - */ - @SuppressLint("SharedFlowCreation") - val finishedKeyguardState: SharedFlow<KeyguardState> = + val startedKeyguardTransitionStep: StateFlow<TransitionStep> = repository.transitions - .transform { step -> - if (step.transitionState == TransitionState.FINISHED) { - emit(step.to) - } - } - .buffer(2, BufferOverflow.DROP_OLDEST) - .shareIn(scope, SharingStarted.Eagerly, replay = 1) + .filter { step -> step.transitionState == TransitionState.STARTED } + .stateIn(scope, SharingStarted.Eagerly, TransitionStep()) /** * The [KeyguardState] we're currently in. @@ -409,8 +353,7 @@ constructor( it.from } } - .distinctUntilChanged() - .stateIn(scope, SharingStarted.Eagerly, KeyguardState.OFF) + .stateIn(scope, SharingStarted.Eagerly, OFF) val isInTransition = combine( @@ -438,8 +381,8 @@ constructor( fromAlternateBouncerTransitionInteractor.get().dismissAlternateBouncer() AOD -> fromAodTransitionInteractor.get().dismissAod() DOZING -> fromDozingTransitionInteractor.get().dismissFromDozing() - KeyguardState.OCCLUDED -> fromOccludedTransitionInteractor.get().dismissFromOccluded() - KeyguardState.GONE -> + OCCLUDED -> fromOccludedTransitionInteractor.get().dismissFromOccluded() + GONE -> Log.i( TAG, "Already transitioning to GONE; ignoring startDismissKeyguardTransition." @@ -506,12 +449,13 @@ constructor( fun isFinishedIn(scene: SceneKey, stateWithoutSceneContainer: KeyguardState): Flow<Boolean> { return if (SceneContainerFlag.isEnabled) { - sceneInteractor.transitionState - .map { it.isIdle(scene) || it.isTransitioning(from = scene) } - .distinctUntilChanged() - } else { - isFinishedIn(stateWithoutSceneContainer) - } + sceneInteractor.transitionState.map { + it.isIdle(scene) || it.isTransitioning(from = scene) + } + } else { + isFinishedIn(stateWithoutSceneContainer) + } + .distinctUntilChanged() } /** Whether we've FINISHED a transition to a state */ @@ -524,13 +468,11 @@ constructor( return currentKeyguardState.replayCache.last() } - fun getStartedFromState(): KeyguardState { - return startedKeyguardFromState.replayCache.last() - } - - fun getFinishedState(): KeyguardState { - return finishedKeyguardState.replayCache.last() - } + private val finishedKeyguardState: StateFlow<KeyguardState> = + repository.transitions + .filter { it.transitionState == TransitionState.FINISHED } + .map { it.to } + .stateIn(scope, SharingStarted.Eagerly, OFF) companion object { private val TAG = KeyguardTransitionInteractor::class.simpleName diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt index 47818cbfd2f2..e00e33df62eb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt @@ -45,6 +45,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -76,7 +77,7 @@ constructor( private val disableFlagsForUserId = combine( selectedUserInteractor.selectedUser, - keyguardTransitionInteractor.startedKeyguardState, + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to }, deviceConfigInteractor.property( namespace = DeviceConfig.NAMESPACE_SYSTEMUI, name = SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt index 906d58664de9..e404f273a768 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt @@ -22,12 +22,12 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.Utils.Companion.sample +import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject /** * Handles logic around the swipe to dismiss gesture, where the user swipes up on the dismissable @@ -53,15 +53,15 @@ constructor( val dismissFling = shadeRepository.currentFling .sample( - transitionInteractor.startedKeyguardState, + transitionInteractor.startedKeyguardTransitionStep, keyguardInteractor.isKeyguardDismissible, keyguardInteractor.statusBarState, ) - .filter { (flingInfo, startedState, keyguardDismissable, statusBarState) -> + .filter { (flingInfo, startedStep, keyguardDismissable, statusBarState) -> flingInfo != null && - !flingInfo.expand && - statusBarState != StatusBarState.SHADE_LOCKED && - startedState == KeyguardState.LOCKSCREEN && + !flingInfo.expand && + statusBarState != StatusBarState.SHADE_LOCKED && + startedStep.to == KeyguardState.LOCKSCREEN && keyguardDismissable } .map { (flingInfo, _) -> flingInfo } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index d06ee645652c..ba12e9356ed7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -62,17 +61,6 @@ sealed class TransitionInteractor( abstract fun start() - /* Use background dispatcher for all [KeyguardTransitionInteractor] flows. Necessary because - * the [sample] utility internally runs a collect on the Unconfined dispatcher, resulting - * in continuations on the main thread. We don't want that for classes that inherit from this. - */ - val startedKeyguardTransitionStep = - transitionInteractor.startedKeyguardTransitionStep.flowOn(bgDispatcher) - // The following are MutableSharedFlows, and do not require flowOn - val startedKeyguardState = transitionInteractor.startedKeyguardState - val finishedKeyguardState = transitionInteractor.finishedKeyguardState - val currentKeyguardState = transitionInteractor.currentKeyguardState - suspend fun startTransitionTo( toState: KeyguardState, animator: ValueAnimator? = getDefaultAnimatorForTransitionsToState(toState), @@ -92,17 +80,6 @@ sealed class TransitionInteractor( " $fromState. This should never happen - check currentTransitionInfoInternal" + " or use filterRelevantKeyguardState before starting transitions." ) - - if (fromState == transitionInteractor.finishedKeyguardState.replayCache.last()) { - Log.e( - name, - "This transition would not have been ignored prior to ag/26681239, since we " + - "are FINISHED in $fromState (but have since started another transition). " + - "If ignoring this transition has caused a regression, fix it by ensuring " + - "that transitions are exclusively started from the most recently started " + - "state." - ) - } return null } @@ -207,11 +184,11 @@ sealed class TransitionInteractor( powerInteractor.isAsleep .filter { isAsleep -> isAsleep } .filterRelevantKeyguardState() - .sample(startedKeyguardTransitionStep) + .sample(transitionInteractor.startedKeyguardTransitionStep) .map(modeOnCanceledFromStartedStep) .collect { modeOnCanceled -> startTransitionTo( - toState = transitionInteractor.asleepKeyguardState.value, + toState = keyguardInteractor.asleepKeyguardState.value, modeOnCanceled = modeOnCanceled, ownerReason = "Sleep transition triggered" ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt index 25b2b7cad7ec..ac874005b612 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt @@ -63,10 +63,13 @@ constructor( ) { private val defaultSurfaceBehindVisibility = combine( - transitionInteractor.finishedKeyguardState, + transitionInteractor.isFinishedIn( + scene = Scenes.Gone, + stateWithoutSceneContainer = KeyguardState.GONE + ), wakeToGoneInteractor.canWakeDirectlyToGone, - ) { finishedState, canWakeDirectlyToGone -> - isSurfaceVisible(finishedState) || canWakeDirectlyToGone + ) { isOnGone, canWakeDirectlyToGone -> + isOnGone || canWakeDirectlyToGone } /** @@ -196,18 +199,20 @@ constructor( edge = Edge.create(to = Scenes.Gone), edgeWithoutSceneContainer = Edge.create(to = KeyguardState.GONE) ), - transitionInteractor.finishedKeyguardState, + transitionInteractor.isFinishedIn( + scene = Scenes.Gone, + stateWithoutSceneContainer = KeyguardState.GONE + ), surfaceBehindInteractor.isAnimatingSurface, notificationLaunchAnimationInteractor.isLaunchAnimationRunning, - ) { isInTransitionToGone, finishedState, isAnimatingSurface, notifLaunchRunning -> + ) { isInTransitionToGone, isOnGone, isAnimatingSurface, notifLaunchRunning -> // Using the animation if we're animating it directly, or if the // ActivityLaunchAnimator is in the process of animating it. val animationsRunning = isAnimatingSurface || notifLaunchRunning // We may still be animating the surface after the keyguard is fully GONE, since // some animations (like the translation spring) are not tied directly to the // transition step amount. - isInTransitionToGone || - (finishedState == KeyguardState.GONE && animationsRunning) + isInTransitionToGone || (isOnGone && animationsRunning) } .distinctUntilChanged() } @@ -248,7 +253,7 @@ constructor( // transition. Same for waking directly to gone, due to the lockscreen being // disabled or because the device was woken back up before the lock timeout // duration elapsed. - KeyguardState.lockscreenVisibleInState(KeyguardState.GONE) + false } else if (canWakeDirectlyToGone) { // Never show the lockscreen if we can wake directly to GONE. This means // that the lock timeout has not yet elapsed, or the keyguard is disabled. @@ -274,8 +279,7 @@ constructor( // *not* play the going away animation or related animations. false } else { - // Otherwise, use the visibility of the current state. - KeyguardState.lockscreenVisibleInState(currentState) + currentState != KeyguardState.GONE } } .distinctUntilChanged() @@ -302,10 +306,4 @@ constructor( !BiometricUnlockMode.isWakeAndUnlock(biometricUnlockState.mode) } .distinctUntilChanged() - - companion object { - fun isSurfaceVisible(state: KeyguardState): Boolean { - return !KeyguardState.lockscreenVisibleInState(state) - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt index b8500952d90a..ffd7812166db 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -116,7 +116,7 @@ constructor( } else { val targetState = if (idle.currentScene == Scenes.Lockscreen) { - transitionInteractor.getStartedFromState() + transitionInteractor.startedKeyguardTransitionStep.value.from } else { UNDEFINED } @@ -155,7 +155,7 @@ constructor( val currentToState = internalTransitionInteractor.currentTransitionInfoInternal.value.to if (currentToState == UNDEFINED) { - transitionKtfTo(transitionInteractor.getStartedFromState()) + transitionKtfTo(transitionInteractor.startedKeyguardTransitionStep.value.from) } } startTransitionFromLockscreen() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt index 24db3c2c70a2..080ddfd18370 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -156,12 +156,6 @@ enum class KeyguardState { companion object { - /** Whether the lockscreen is visible when we're FINISHED in the given state. */ - fun lockscreenVisibleInState(state: KeyguardState): Boolean { - // TODO(b/349784682): Transform deprecated states for Flexiglass - return state != GONE - } - /** * Whether the device is awake ([PowerInteractor.isAwake]) when we're FINISHED in the given * keyguard state. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt index 06b76b3c0f37..87c32a54438e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt @@ -53,10 +53,10 @@ constructor( ) { private val isShowingAodOrDozing: Flow<Boolean> = combine( - transitionInteractor.startedKeyguardState, + transitionInteractor.startedKeyguardTransitionStep, transitionInteractor.transitionValue(KeyguardState.DOZING), - ) { startedKeyguardState, dozingTransitionValue -> - startedKeyguardState == KeyguardState.AOD || dozingTransitionValue == 1f + ) { startedKeyguardStep, dozingTransitionValue -> + startedKeyguardStep.to == KeyguardState.AOD || dozingTransitionValue == 1f } private fun getColor(usingBackgroundProtection: Boolean): Int { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 5ce1b5e3dcc5..d3bb4f5d7508 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -83,8 +83,8 @@ constructor( private val intEvaluator = IntEvaluator() private val floatEvaluator = FloatEvaluator() private val showingAlternateBouncer: Flow<Boolean> = - transitionInteractor.startedKeyguardState.map { keyguardState -> - keyguardState == KeyguardState.ALTERNATE_BOUNCER + transitionInteractor.startedKeyguardTransitionStep.map { keyguardStep -> + keyguardStep.to == KeyguardState.ALTERNATE_BOUNCER } private val qsProgress: Flow<Float> = shadeInteractor.qsExpansion.onStart { emit(0f) } private val shadeExpansion: Flow<Float> = shadeInteractor.shadeExpansion.onStart { emit(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt index c885c9a5a29b..fe4ebfedee40 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt @@ -85,9 +85,7 @@ constructor( private val previewMode = MutableStateFlow(PreviewMode()) private val showingLockscreen: Flow<Boolean> = - transitionInteractor.finishedKeyguardState.map { keyguardState -> - keyguardState == KeyguardState.LOCKSCREEN - } + transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN) /** The only time the expansion is important is while lockscreen is actively displayed */ private val shadeExpansionAlpha = 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 a96869df001a..ebdcaa0c91a6 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 @@ -132,8 +132,8 @@ constructor( val burnInModel = _burnInModel.asStateFlow() val burnInLayerVisibility: Flow<Int> = - keyguardTransitionInteractor.startedKeyguardState - .filter { it == AOD || it == LOCKSCREEN } + keyguardTransitionInteractor.startedKeyguardTransitionStep + .filter { it.to == AOD || it.to == LOCKSCREEN } .map { VISIBLE } val goneToAodTransition = @@ -333,16 +333,17 @@ constructor( .transitionValue(LOCKSCREEN) .map { it > 0f } .onStart { emit(false) }, - keyguardTransitionInteractor.finishedKeyguardState.map { - KeyguardState.lockscreenVisibleInState(it) - }, + keyguardTransitionInteractor.isFinishedIn( + scene = Scenes.Gone, + stateWithoutSceneContainer = GONE + ), deviceEntryInteractor.isBypassEnabled, areNotifsFullyHiddenAnimated(), isPulseExpandingAnimated(), ) { flows -> val goneToAodTransitionRunning = flows[0] as Boolean val isOnLockscreen = flows[1] as Boolean - val onKeyguard = flows[2] as Boolean + val isOnGone = flows[2] as Boolean val isBypassEnabled = flows[3] as Boolean val notifsFullyHidden = flows[4] as AnimatedValue<Boolean> val pulseExpanding = flows[5] as AnimatedValue<Boolean> @@ -352,8 +353,7 @@ constructor( // animation is playing, in which case we want them to be visible if we're // animating in the AOD UI and will be switching to KEYGUARD shortly. goneToAodTransitionRunning || - (!onKeyguard && - !screenOffAnimationController.shouldShowAodIconsWhenShade()) -> + (isOnGone && !screenOffAnimationController.shouldShowAodIconsWhenShade()) -> AnimatedValue.NotAnimating(false) else -> zip(notifsFullyHidden, pulseExpanding) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 2b6c3c080b78..fcafd5e9151d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -25,7 +25,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteract import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.shared.model.Scenes @@ -60,7 +59,7 @@ constructor( private val unfoldTransitionInteractor: UnfoldTransitionInteractor, private val occlusionInteractor: SceneContainerOcclusionInteractor, private val deviceEntryInteractor: DeviceEntryInteractor, -) : SysUiViewModel, ExclusiveActivatable() { +) : ExclusiveActivatable() { @VisibleForTesting val clockSize = clockInteractor.clockSize val isUdfpsVisible: Boolean diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt index e64c61490204..c0b9efaaec01 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt @@ -23,7 +23,9 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.composable.transitions.FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -51,10 +53,18 @@ constructor( edge = Edge.create(from = LOCKSCREEN, to = PRIMARY_BOUNCER), ) + private val alphaForAnimationStep: (Float) -> Float = + when { + SceneContainerFlag.isEnabled -> { step -> + 1f - Math.min((step / FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION), 1f) + } + else -> { step -> 1f - step } + } + val shortcutsAlpha: Flow<Float> = transitionAnimation.sharedFlow( duration = FromLockscreenTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, - onStep = { 1f - it } + onStep = alphaForAnimationStep ) val lockscreenAlpha: Flow<Float> = shortcutsAlpha diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ShadeDependentFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ShadeDependentFlows.kt index e45d537155fd..708b4085da7f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ShadeDependentFlows.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ShadeDependentFlows.kt @@ -34,7 +34,9 @@ constructor( ) { /** When the last keyguard state transition started, was the shade fully expanded? */ private val lastStartedTransitionHadShadeFullyExpanded: Flow<Boolean> = - transitionInteractor.startedKeyguardState.sample(shadeInteractor.isAnyFullyExpanded) + transitionInteractor.startedKeyguardTransitionStep.sample( + shadeInteractor.isAnyFullyExpanded + ) /** * Decide which flow to use depending on the shade expansion state at the start of the last diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/Activatable.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/Activatable.kt index bd3d40b114e3..c1768a4d903c 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/Activatable.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/Activatable.kt @@ -19,6 +19,7 @@ package com.android.systemui.lifecycle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import com.android.app.tracing.coroutines.traceCoroutine /** Defines interface for classes that can be activated to do coroutine work. */ interface Activatable { @@ -66,13 +67,19 @@ interface Activatable { * * If the [key] changes, the old [Activatable] is deactivated and a new one will be instantiated, * activated, and returned. + * + * The [traceName] is used for coroutine performance tracing purposes. Please try to use a label + * that's unique enough and easy enough to find in code search; this should help correlate + * performance findings with actual code. One recommendation: prefer whole string literals instead + * of some complex concatenation or templating scheme. */ @Composable fun <T : Activatable> rememberActivated( + traceName: String, key: Any = Unit, factory: () -> T, ): T { val instance = remember(key) { factory() } - LaunchedEffect(instance) { instance.activate() } + LaunchedEffect(instance) { traceCoroutine(traceName) { instance.activate() } } return instance } diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt index 59ec2af2d697..df1394bbfa5f 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/Hydrator.kt @@ -19,6 +19,8 @@ package com.android.systemui.lifecycle import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.StateFactoryMarker +import com.android.app.tracing.coroutines.launch +import com.android.app.tracing.coroutines.traceCoroutine import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -37,35 +39,55 @@ import kotlinx.coroutines.launch * } * ``` */ -class Hydrator : ExclusiveActivatable() { +class Hydrator( + /** + * A name for performance tracing purposes. + * + * Please use a short string literal that's easy to find in code search. Try to avoid + * concatenation or templating. + */ + private val traceName: String, +) : ExclusiveActivatable() { - private val children = mutableListOf<Activatable>() + private val children = mutableListOf<NamedActivatable>() /** - * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active. + * Returns a snapshot [State] that's kept up-to-date as long as its owner is active. * + * @param traceName Used for coroutine performance tracing purposes. Please try to use a label + * that's unique enough and easy enough to find in code search; this should help correlate + * performance findings with actual code. One recommendation: prefer whole string literals + * instead of some complex concatenation or templating scheme. * @param source The upstream [StateFlow] to collect from; values emitted to it will be * automatically set on the returned [State]. */ @StateFactoryMarker fun <T> hydratedStateOf( + traceName: String, source: StateFlow<T>, ): State<T> { return hydratedStateOf( + traceName = traceName, initialValue = source.value, source = source, ) } /** - * Returns a snapshot [State] that's kept up-to-date as long as the [SysUiViewModel] is active. + * Returns a snapshot [State] that's kept up-to-date as long as its owner is active. * + * @param traceName Used for coroutine performance tracing purposes. Please try to use a label + * that's unique enough and easy enough to find in code search; this should help correlate + * performance findings with actual code. One recommendation: prefer whole string literals + * instead of some complex concatenation or templating scheme. Use `null` to disable + * performance tracing for this state. * @param initialValue The first value to place on the [State] * @param source The upstream [Flow] to collect from; values emitted to it will be automatically * set on the returned [State]. */ @StateFactoryMarker fun <T> hydratedStateOf( + traceName: String?, initialValue: T, source: Flow<T>, ): State<T> { @@ -73,18 +95,35 @@ class Hydrator : ExclusiveActivatable() { val mutableState = mutableStateOf(initialValue) children.add( - object : ExclusiveActivatable() { - override suspend fun onActivated(): Nothing { - source.collect { mutableState.value = it } - awaitCancellation() - } - } + NamedActivatable( + traceName = traceName, + activatable = + object : ExclusiveActivatable() { + override suspend fun onActivated(): Nothing { + source.collect { mutableState.value = it } + awaitCancellation() + } + }, + ) ) return mutableState } override suspend fun onActivated() = coroutineScope { - children.forEach { child -> launch { child.activate() } } - awaitCancellation() + traceCoroutine(traceName) { + children.forEach { child -> + if (child.traceName != null) { + launch(spanName = child.traceName) { child.activatable.activate() } + } else { + launch { child.activatable.activate() } + } + } + awaitCancellation() + } } + + private data class NamedActivatable( + val traceName: String?, + val activatable: Activatable, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt index 29ffcbd15125..508b04eee91a 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt @@ -20,36 +20,46 @@ import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import com.android.app.tracing.coroutines.traceCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -/** Defines interface for all System UI view-models. */ -interface SysUiViewModel - /** - * Returns a remembered [SysUiViewModel] of the type [T]. If the returned instance is also an + * Returns a remembered view-model of the type [T]. If the returned instance is also an * [Activatable], it's automatically kept active until this composable leaves the composition; if - * the [key] changes, the old [SysUiViewModel] is deactivated and a new one will be instantiated, + * the [key] changes, the old view-model is deactivated and a new one will be instantiated, * activated, and returned. + * + * The [traceName] is used for coroutine performance tracing purposes. Please try to use a label + * that's unique enough and easy enough to find in code search; this should help correlate + * performance findings with actual code. One recommendation: prefer whole string literals instead + * of some complex concatenation or templating scheme. */ @Composable -fun <T : SysUiViewModel> rememberViewModel( +fun <T> rememberViewModel( + traceName: String, key: Any = Unit, factory: () -> T, ): T { val instance = remember(key) { factory() } if (instance is Activatable) { - LaunchedEffect(instance) { instance.activate() } + LaunchedEffect(instance) { traceCoroutine(traceName) { instance.activate() } } } return instance } /** - * Invokes [block] in a new coroutine with a new [SysUiViewModel] that is automatically activated - * whenever `this` [View]'s Window's [WindowLifecycleState] is at least at - * [minWindowLifecycleState], and is automatically canceled once that is no longer the case. + * Invokes [block] in a new coroutine with a new view-model that is automatically activated whenever + * `this` [View]'s Window's [WindowLifecycleState] is at least at [minWindowLifecycleState], and is + * automatically canceled once that is no longer the case. + * + * The [traceName] is used for coroutine performance tracing purposes. Please try to use a label + * that's unique enough and easy enough to find in code search; this should help correlate + * performance findings with actual code. One recommendation: prefer whole string literals instead + * of some complex concatenation or templating scheme. */ -suspend fun <T : SysUiViewModel> View.viewModel( +suspend fun <T> View.viewModel( + traceName: String, minWindowLifecycleState: WindowLifecycleState, factory: () -> T, block: suspend CoroutineScope.(T) -> Unit, @@ -57,7 +67,7 @@ suspend fun <T : SysUiViewModel> View.viewModel( repeatOnWindowLifecycle(minWindowLifecycleState) { val instance = factory() if (instance is Activatable) { - launch { instance.activate() } + launch { traceCoroutine(traceName) { instance.activate() } } } block(instance) } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index ba3c1d216099..ed766469094e 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -382,6 +382,16 @@ public class LogModule { return factory.create("MediaLog", 20); } + /** + * Provides a buffer for media device changes + */ + @Provides + @SysUISingleton + @MediaDeviceLog + public static LogBuffer providesMediaDeviceLogBuffer(LogBufferFactory factory) { + return factory.create("MediaDeviceLog", 50); + } + /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaDeviceLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaDeviceLog.kt new file mode 100644 index 000000000000..06bd26971232 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaDeviceLog.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger + +import com.android.systemui.log.LogBuffer +import javax.inject.Qualifier + +/** A [LogBuffer] for [com.android.systemui.media.controls.domain.pipeline.MediaDeviceLogger] */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class MediaDeviceLog diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index 70189b79ea58..378a147c2c82 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -30,6 +30,7 @@ import android.util.Log import androidx.media.utils.MediaConstants import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS +import com.android.systemui.media.controls.shared.MediaControlDrawables import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.plugins.ActivityStarter @@ -58,14 +59,13 @@ fun createActionsFromState( val playOrPause = if (isConnectingState(state.state)) { // Spinner needs to be animating to render anything. Start it here. - val drawable = - context.getDrawable(com.android.internal.R.drawable.progress_small_material) + val drawable = MediaControlDrawables.getProgress(context) (drawable as Animatable).start() MediaAction( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + MediaControlDrawables.getConnecting(context), // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material ) @@ -153,23 +153,23 @@ private fun getStandardAction( return when (action) { PlaybackState.ACTION_PLAY -> { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + MediaControlDrawables.getPlayIcon(context), { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container) + MediaControlDrawables.getPlayBackground(context) ) } PlaybackState.ACTION_PAUSE -> { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + MediaControlDrawables.getPauseIcon(context), { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container) + MediaControlDrawables.getPauseBackground(context) ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { MediaAction( - context.getDrawable(R.drawable.ic_media_prev), + MediaControlDrawables.getPrevIcon(context), { controller.transportControls.skipToPrevious() }, context.getString(R.string.controls_media_button_prev), null @@ -177,7 +177,7 @@ private fun getStandardAction( } PlaybackState.ACTION_SKIP_TO_NEXT -> { MediaAction( - context.getDrawable(R.drawable.ic_media_next), + MediaControlDrawables.getNextIcon(context), { controller.transportControls.skipToNext() }, context.getString(R.string.controls_media_button_next), null diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index 916f8b2e1730..415449f4454f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -16,9 +16,8 @@ package com.android.systemui.media.controls.domain.pipeline +import android.annotation.MainThread import android.annotation.SuppressLint -import android.app.ActivityOptions -import android.app.BroadcastOptions import android.app.Notification import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME import android.app.PendingIntent @@ -39,7 +38,6 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.ImageDecoder -import android.graphics.drawable.Animatable import android.graphics.drawable.Icon import android.media.MediaDescription import android.media.MediaMetadata @@ -47,7 +45,6 @@ import android.media.session.MediaController import android.media.session.MediaSession import android.media.session.PlaybackState import android.net.Uri -import android.os.Handler import android.os.Parcelable import android.os.Process import android.os.UserHandle @@ -63,6 +60,7 @@ import com.android.internal.annotations.Keep import com.android.internal.logging.InstanceId import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.CoreStartable +import com.android.systemui.Flags import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -92,7 +90,6 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.BcSmartspaceDataPlugin import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert @@ -137,7 +134,7 @@ class MediaDataProcessor( @Background private val backgroundExecutor: Executor, @Main private val uiExecutor: Executor, @Main private val foregroundExecutor: DelayableExecutor, - @Main private val handler: Handler, + @Main private val mainDispatcher: CoroutineDispatcher, private val mediaControllerFactory: MediaControllerFactory, private val broadcastDispatcher: BroadcastDispatcher, private val dumpManager: DumpManager, @@ -152,6 +149,7 @@ class MediaDataProcessor( private val smartspaceManager: SmartspaceManager?, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val mediaDataRepository: MediaDataRepository, + private val mediaDataLoader: dagger.Lazy<MediaDataLoader>, ) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener { companion object { @@ -217,7 +215,7 @@ class MediaDataProcessor( threadFactory: ThreadFactory, @Main uiExecutor: Executor, @Main foregroundExecutor: DelayableExecutor, - @Main handler: Handler, + @Main mainDispatcher: CoroutineDispatcher, mediaControllerFactory: MediaControllerFactory, dumpManager: DumpManager, broadcastDispatcher: BroadcastDispatcher, @@ -230,6 +228,7 @@ class MediaDataProcessor( smartspaceManager: SmartspaceManager?, keyguardUpdateMonitor: KeyguardUpdateMonitor, mediaDataRepository: MediaDataRepository, + mediaDataLoader: dagger.Lazy<MediaDataLoader>, ) : this( context, applicationScope, @@ -239,7 +238,7 @@ class MediaDataProcessor( threadFactory.buildExecutorOnNewThread(TAG), uiExecutor, foregroundExecutor, - handler, + mainDispatcher, mediaControllerFactory, broadcastDispatcher, dumpManager, @@ -254,6 +253,7 @@ class MediaDataProcessor( smartspaceManager, keyguardUpdateMonitor, mediaDataRepository, + mediaDataLoader, ) private val appChangeReceiver = @@ -436,16 +436,30 @@ class MediaDataProcessor( logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) logger.logResumeMediaAdded(appUid, packageName, instanceId) } - backgroundExecutor.execute { - loadMediaDataInBgForResumption( - userId, - desc, - action, - token, - appName, - appIntent, - packageName - ) + if (Flags.mediaLoadMetadataViaMediaDataLoader()) { + applicationScope.launch { + loadMediaDataForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + } else { + backgroundExecutor.execute { + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } } } @@ -471,7 +485,13 @@ class MediaDataProcessor( oldKey: String?, isNewlyActiveEntry: Boolean = false, ) { - backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + if (Flags.mediaLoadMetadataViaMediaDataLoader()) { + applicationScope.launch { + loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry) + } + } else { + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + } } /** Add a listener for internal events. */ @@ -646,6 +666,75 @@ class MediaDataProcessor( } } + private suspend fun loadMediaDataForResumption( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) = + withContext(backgroundDispatcher) { + val lastActive = systemClock.elapsedRealtime() + val currentEntry = mediaDataRepository.mediaEntries.value[packageName] + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + val result = + mediaDataLoader + .get() + .loadMediaDataForResumption( + userId, + desc, + resumeAction, + currentEntry, + token, + appName, + appIntent, + packageName + ) + if (result == null || desc.title.isNullOrBlank()) { + Log.d(TAG, "No MediaData result for resumption") + mediaDataRepository.removeMediaEntry(packageName) + return@withContext + } + + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + withContext(mainDispatcher) { + onMediaDataLoaded( + packageName, + null, + MediaData( + userId = userId, + initialized = true, + app = result.appName, + appIcon = null, + artist = result.artist, + song = result.song, + artwork = result.artworkIcon, + actions = result.actionIcons, + actionsToShowInCompact = result.actionsToShowInCompact, + semanticActions = result.semanticActions, + packageName = packageName, + token = result.token, + clickIntent = result.clickIntent, + device = result.device, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = result.appUid, + isExplicit = result.isExplicit, + resumeProgress = result.resumeProgress, + ) + ) + } + } + + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun loadMediaDataInBgForResumption( userId: Int, desc: MediaDescription, @@ -730,6 +819,82 @@ class MediaDataProcessor( } } + private suspend fun loadMediaDataWithLoader( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) = + withContext(backgroundDispatcher) { + val lastActive = systemClock.elapsedRealtime() + val result = mediaDataLoader.get().loadMediaData(key, sbn) + if (result == null) { + Log.d(TAG, "No result from loadMediaData") + return@withContext + } + + val currentEntry = mediaDataRepository.mediaEntries.value[key] + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + val resumeAction: Runnable? = currentEntry?.resumeAction + val hasCheckedForResume = currentEntry?.hasCheckedForResume == true + val active = currentEntry?.active ?: true + + // We need to log the correct media added. + if (isNewlyActiveEntry) { + logSingleVsMultipleMediaAdded(result.appUid, sbn.packageName, instanceId) + logger.logActiveMediaAdded( + result.appUid, + sbn.packageName, + instanceId, + result.playbackLocation + ) + } else if (result.playbackLocation != currentEntry?.playbackLocation) { + logger.logPlaybackLocationChange( + result.appUid, + sbn.packageName, + instanceId, + result.playbackLocation + ) + } + + withContext(mainDispatcher) { + onMediaDataLoaded( + key, + oldKey, + MediaData( + userId = sbn.normalizedUserId, + initialized = true, + app = result.appName, + appIcon = result.appIcon, + artist = result.artist, + song = result.song, + artwork = result.artworkIcon, + actions = result.actionIcons, + actionsToShowInCompact = result.actionsToShowInCompact, + semanticActions = result.semanticActions, + packageName = sbn.packageName, + token = result.token, + clickIntent = result.clickIntent, + device = result.device, + active = active, + resumeAction = resumeAction, + playbackLocation = result.playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = result.isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = result.appUid, + isExplicit = result.isExplicit, + ) + ) + } + } + + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") fun loadMediaDataInBg( key: String, sbn: StatusBarNotification, @@ -843,7 +1008,7 @@ class MediaDataProcessor( var actionsToShowCollapsed: List<Int> = emptyList() val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) if (semanticActions == null) { - val actions = createActionsFromNotification(sbn) + val actions = createActionsFromNotification(context, activityStarter, sbn) actionIcons = actions.first actionsToShowCollapsed = actions.second } @@ -926,6 +1091,7 @@ class MediaDataProcessor( } } + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { try { return context.packageManager.getApplicationInfo(packageName, 0) @@ -935,6 +1101,7 @@ class MediaDataProcessor( return null } + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) if (name != null) { @@ -948,78 +1115,6 @@ class MediaDataProcessor( } } - /** Generate action buttons based on notification actions */ - private fun createActionsFromNotification( - sbn: StatusBarNotification - ): Pair<List<MediaAction>, List<Int>> { - val notif = sbn.notification - val actionIcons: MutableList<MediaAction> = ArrayList() - val actions = notif.actions - var actionsToShowCollapsed = - notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() - ?: mutableListOf() - if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { - Log.e( - TAG, - "Too many compact actions for ${sbn.key}," + - "limiting to first $MAX_COMPACT_ACTIONS" - ) - actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) - } - - if (actions != null) { - for ((index, action) in actions.withIndex()) { - if (index == MAX_NOTIFICATION_ACTIONS) { - Log.w( - TAG, - "Too many notification actions for ${sbn.key}," + - " limiting to first $MAX_NOTIFICATION_ACTIONS" - ) - break - } - if (action.getIcon() == null) { - if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") - actionsToShowCollapsed.remove(index) - continue - } - val runnable = - if (action.actionIntent != null) { - Runnable { - if (action.actionIntent.isActivity) { - activityStarter.startPendingIntentDismissingKeyguard( - action.actionIntent - ) - } else if (action.isAuthenticationRequired()) { - activityStarter.dismissKeyguardThenExecute( - { - var result = sendPendingIntent(action.actionIntent) - result - }, - {}, - true - ) - } else { - sendPendingIntent(action.actionIntent) - } - } - } else { - null - } - val mediaActionIcon = - if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { - Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) - } else { - action.getIcon() - } - .setTint(themeText) - .loadDrawable(context) - val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) - actionIcons.add(mediaAction) - } - } - return Pair(actionIcons, actionsToShowCollapsed) - } - /** * Generates action button info for this media session based on the PlaybackState * @@ -1036,174 +1131,14 @@ class MediaDataProcessor( controller: MediaController, user: UserHandle ): MediaButton? { - val state = controller.playbackState - if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { + if (!mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { return null } - - // First, check for standard actions - val playOrPause = - if (isConnectingState(state.state)) { - // Spinner needs to be animating to render anything. Start it here. - val drawable = MediaControlDrawables.getProgress(context) - (drawable as Animatable).start() - MediaAction( - drawable, - null, // no action to perform when clicked - context.getString(R.string.controls_media_button_connecting), - MediaControlDrawables.getConnecting(context), - // Specify a rebind id to prevent the spinner from restarting on later binds. - com.android.internal.R.drawable.progress_small_material - ) - } else if (isPlayingState(state.state)) { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) - } else { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) - } - val prevButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) - val nextButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) - - // Then, create a way to build any custom actions that will be needed - val customActions = - state.customActions - .asSequence() - .filterNotNull() - .map { getCustomAction(packageName, controller, it) } - .iterator() - fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null - - // Finally, assign the remaining button slots: play/pause A B C D - // A = previous, else custom action (if not reserved) - // B = next, else custom action (if not reserved) - // C and D are always custom actions - val reservePrev = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV - ) == true - val reserveNext = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT - ) == true - - val prevOrCustom = - if (prevButton != null) { - prevButton - } else if (!reservePrev) { - nextCustomAction() - } else { - null - } - - val nextOrCustom = - if (nextButton != null) { - nextButton - } else if (!reserveNext) { - nextCustomAction() - } else { - null - } - - return MediaButton( - playOrPause, - nextOrCustom, - prevOrCustom, - nextCustomAction(), - nextCustomAction(), - reserveNext, - reservePrev - ) - } - - /** - * Create a [MediaAction] for a given action and media session - * - * @param controller MediaController for the session - * @param stateActions The actions included with the session's [PlaybackState] - * @param action A [PlaybackState.Actions] value representing what action to generate. One of: - * ``` - * [PlaybackState.ACTION_PLAY] - * [PlaybackState.ACTION_PAUSE] - * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] - * [PlaybackState.ACTION_SKIP_TO_NEXT] - * @return - * ``` - * - * A [MediaAction] with correct values set, or null if the state doesn't support it - */ - private fun getStandardAction( - controller: MediaController, - stateActions: Long, - @PlaybackState.Actions action: Long - ): MediaAction? { - if (!includesAction(stateActions, action)) { - return null - } - - return when (action) { - PlaybackState.ACTION_PLAY -> { - MediaAction( - MediaControlDrawables.getPlayIcon(context), - { controller.transportControls.play() }, - context.getString(R.string.controls_media_button_play), - MediaControlDrawables.getPlayBackground(context) - ) - } - PlaybackState.ACTION_PAUSE -> { - MediaAction( - MediaControlDrawables.getPauseIcon(context), - { controller.transportControls.pause() }, - context.getString(R.string.controls_media_button_pause), - MediaControlDrawables.getPauseBackground(context) - ) - } - PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { - MediaAction( - MediaControlDrawables.getPrevIcon(context), - { controller.transportControls.skipToPrevious() }, - context.getString(R.string.controls_media_button_prev), - null - ) - } - PlaybackState.ACTION_SKIP_TO_NEXT -> { - MediaAction( - MediaControlDrawables.getNextIcon(context), - { controller.transportControls.skipToNext() }, - context.getString(R.string.controls_media_button_next), - null - ) - } - else -> null - } - } - - /** Check whether the actions from a [PlaybackState] include a specific action */ - private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { - if ( - (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && - (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) - ) { - return true - } - return (stateActions and action != 0L) - } - - /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ - private fun getCustomAction( - packageName: String, - controller: MediaController, - customAction: PlaybackState.CustomAction - ): MediaAction { - return MediaAction( - Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), - { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, - customAction.name, - null - ) + return createActionsFromState(context, packageName, controller) } /** Load a bitmap from the various Art metadata URIs */ + @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { for (uri in ART_URIS) { val uriString = metadata.getString(uri) @@ -1218,21 +1153,6 @@ class MediaDataProcessor( return null } - private fun sendPendingIntent(intent: PendingIntent): Boolean { - return try { - val options = BroadcastOptions.makeBasic() - options.setInteractive(true) - options.setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED - ) - intent.send(options.toBundle()) - true - } catch (e: PendingIntent.CanceledException) { - Log.d(TAG, "Intent canceled", e) - false - } - } - /** Returns a bitmap if the user can access the given URI, else null */ private fun loadBitmapFromUriForUser( uri: Uri, @@ -1313,6 +1233,7 @@ class MediaDataProcessor( ) } + @MainThread fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = traceSection("MediaDataProcessor#onMediaDataLoaded") { Assert.isMainThread() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceLogger.kt new file mode 100644 index 000000000000..f886166b986b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceLogger.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.systemui.media.controls.domain.pipeline + +import android.media.session.MediaController +import com.android.settingslib.media.MediaDevice +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.MediaDeviceLog +import com.android.systemui.media.controls.shared.model.MediaDeviceData +import javax.inject.Inject + +/** A [LogBuffer] for media device changes */ +class MediaDeviceLogger @Inject constructor(@MediaDeviceLog private val buffer: LogBuffer) { + + fun logBroadcastEvent(event: String, reason: Int, broadcastId: Int) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = event + int1 = reason + int2 = broadcastId + }, + { "$str1, reason = $int1, broadcastId = $int2" } + ) + } + + fun logBroadcastEvent(event: String, reason: Int) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = event + int1 = reason + }, + { "$str1, reason = $int1" } + ) + } + + fun logBroadcastMetadataChanged(broadcastId: Int, metadata: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + int1 = broadcastId + str1 = metadata + }, + { "onBroadcastMetadataChanged, broadcastId = $int1, metadata = $str1" } + ) + } + + fun logNewDeviceName(name: String?) { + buffer.log(TAG, LogLevel.DEBUG, { str1 = name }, { "New device name $str1" }) + } + + fun logLocalDevice(sassDevice: MediaDeviceData?, connectedDevice: MediaDeviceData?) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = sassDevice?.name?.toString() + str2 = connectedDevice?.name?.toString() + }, + { "Local device: $str1 or $str2" } + ) + } + + fun logRemoteDevice(routingSessionName: CharSequence?, connectedDevice: MediaDeviceData?) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = routingSessionName?.toString() + str2 = connectedDevice?.name?.toString() + }, + { "Remote device: $str1 or $str2 or unknown" } + ) + } + + fun logDeviceName( + device: MediaDevice?, + controller: MediaController?, + routingSessionName: CharSequence?, + selectedRouteName: CharSequence? + ) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = "device $device, controller: $controller" + str2 = routingSessionName?.toString() + str3 = selectedRouteName?.toString() + }, + { "$str1, routingSession $str2 or selected route $str3" } + ) + } + + companion object { + private const val TAG = "MediaDeviceLog" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index a193f7f8f498..49b53c2d78ae 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -71,6 +71,7 @@ constructor( private val localBluetoothManager: Lazy<LocalBluetoothManager?>, @Main private val fgExecutor: Executor, @Background private val bgExecutor: Executor, + private val logger: MediaDeviceLogger, ) : MediaDataManager.Listener { private val listeners: MutableSet<Listener> = mutableSetOf() @@ -281,59 +282,38 @@ constructor( } override fun onBroadcastStarted(reason: Int, broadcastId: Int) { - if (DEBUG) { - Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId") - } + logger.logBroadcastEvent("onBroadcastStarted", reason, broadcastId) updateCurrent() } override fun onBroadcastStartFailed(reason: Int) { - if (DEBUG) { - Log.d(TAG, "onBroadcastStartFailed(), reason = $reason") - } + logger.logBroadcastEvent("onBroadcastStartFailed", reason) } override fun onBroadcastMetadataChanged( broadcastId: Int, metadata: BluetoothLeBroadcastMetadata ) { - if (DEBUG) { - Log.d( - TAG, - "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + - "metadata = $metadata" - ) - } + logger.logBroadcastMetadataChanged(broadcastId, metadata.toString()) updateCurrent() } override fun onBroadcastStopped(reason: Int, broadcastId: Int) { - if (DEBUG) { - Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId") - } + logger.logBroadcastEvent("onBroadcastStopped", reason, broadcastId) updateCurrent() } override fun onBroadcastStopFailed(reason: Int) { - if (DEBUG) { - Log.d(TAG, "onBroadcastStopFailed(), reason = $reason") - } + logger.logBroadcastEvent("onBroadcastStopFailed", reason) } override fun onBroadcastUpdated(reason: Int, broadcastId: Int) { - if (DEBUG) { - Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId") - } + logger.logBroadcastEvent("onBroadcastUpdated", reason, broadcastId) updateCurrent() } override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) { - if (DEBUG) { - Log.d( - TAG, - "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId" - ) - } + logger.logBroadcastEvent("onBroadcastUpdateFailed", reason, broadcastId) } override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} @@ -381,12 +361,16 @@ constructor( name = context.getString(R.string.media_seamless_other_device), showBroadcastButton = false ) + logger.logRemoteDevice(routingSession?.name, connectedDevice) } else { // Prefer SASS if available when playback is local. - activeDevice = getSassDevice() ?: connectedDevice + val sassDevice = getSassDevice() + activeDevice = sassDevice ?: connectedDevice + logger.logLocalDevice(sassDevice, connectedDevice) } current = activeDevice ?: EMPTY_AND_DISABLED_MEDIA_DEVICE_DATA + logger.logNewDeviceName(current?.name?.toString()) } else { val aboutToConnect = aboutToConnectDeviceOverride if ( @@ -407,9 +391,7 @@ constructor( val enabled = device != null && (controller == null || routingSession != null) val name = getDeviceName(device, routingSession) - if (DEBUG) { - Log.d(TAG, "new device name $name") - } + logger.logNewDeviceName(name) current = MediaDeviceData( enabled, @@ -463,14 +445,12 @@ constructor( ): String? { val selectedRoutes = routingSession?.let { mr2manager.get().getSelectedRoutes(it) } - if (DEBUG) { - Log.d( - TAG, - "device is $device, controller $controller," + - " routingSession ${routingSession?.name}" + - " or ${selectedRoutes?.firstOrNull()?.name}" - ) - } + logger.logDeviceName( + device, + controller, + routingSession?.name, + selectedRoutes?.firstOrNull()?.name + ) if (controller == null) { // In resume state, we don't have a controller - just use the device name diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index 8fd578e99b33..bf9ef8c5d24e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -49,7 +49,6 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge -import com.android.systemui.keyguard.shared.model.KeyguardState 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.TransitionState @@ -105,12 +104,14 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -166,6 +167,9 @@ constructor( /** Is the player currently visible (at the end of the transformation */ private var playersVisible: Boolean = false + /** Are we currently disabling pagination only allowing one media session to show */ + private var currentlyDisablePagination: Boolean = false + /** * The desired location where we'll be at the end of the transformation. Usually this matches * the end location, except when we're still waiting on a state update call. @@ -329,6 +333,11 @@ constructor( private val controllerById = mutableMapOf<String, MediaViewController>() private val commonViewModels = mutableListOf<MediaCommonViewModel>() + private val isOnGone = + keyguardTransitionInteractor + .isFinishedIn(Scenes.Gone, GONE) + .stateIn(applicationScope, SharingStarted.Eagerly, true) + init { dumpManager.registerDumpable(TAG, this) mediaFrame = inflateMediaCarousel() @@ -910,9 +919,7 @@ constructor( if (SceneContainerFlag.isEnabled) { !deviceEntryInteractor.isDeviceEntered.value } else { - KeyguardState.lockscreenVisibleInState( - keyguardTransitionInteractor.getFinishedState() - ) + !isOnGone.value } return !allowMediaPlayerOnLockScreen && isOnLockscreen } @@ -1355,14 +1362,20 @@ constructor( val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive + val startDisablePagination = hostStates[currentStartLocation]?.disablePagination ?: false + val endDisablePagination = hostStates[currentEndLocation]?.disablePagination ?: false + if ( currentlyShowingOnlyActive != endShowsActive || + currentlyDisablePagination != endDisablePagination || ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && - startShowsActive != endShowsActive) + (startShowsActive != endShowsActive || + startDisablePagination != endDisablePagination)) ) { // Whenever we're transitioning from between differing states or the endstate differs // we reset the translation currentlyShowingOnlyActive = endShowsActive + currentlyDisablePagination = endDisablePagination mediaCarouselScrollHandler.resetTranslation(animate = true) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt index 91050c8bfab3..09a618110f21 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt @@ -44,6 +44,7 @@ class MediaHost( lateinit var hostView: UniqueObjectHostView var location: Int = -1 private set + private var visibleChangedListeners: ArraySet<(Boolean) -> Unit> = ArraySet() private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0) @@ -287,6 +288,15 @@ class MediaHost( changedListener?.invoke() } + override var disablePagination: Boolean = false + set(value) { + if (field == value) { + return + } + field = value + changedListener?.invoke() + } + private var lastDisappearHash = disappearParameters.hashCode() /** A listener for all changes. This won't be copied over when invoking [copy] */ @@ -303,6 +313,7 @@ class MediaHost( mediaHostState.visible = visible mediaHostState.disappearParameters = disappearParameters.deepCopy() mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded + mediaHostState.disablePagination = disablePagination return mediaHostState } @@ -331,6 +342,9 @@ class MediaHost( if (!disappearParameters.equals(other.disappearParameters)) { return false } + if (disablePagination != other.disablePagination) { + return false + } return true } @@ -342,6 +356,7 @@ class MediaHost( result = 31 * result + showsOnlyActiveMedia.hashCode() result = 31 * result + if (visible) 1 else 2 result = 31 * result + disappearParameters.hashCode() + result = 31 * result + disablePagination.hashCode() return result } } @@ -400,6 +415,12 @@ interface MediaHostState { */ var disappearParameters: DisappearParameters + /** + * Whether pagination should be disabled for this host, meaning that when there are multiple + * media sessions, only the first one will appear. + */ + var disablePagination: Boolean + /** Get a copy of this view state, deepcopying all appropriate members */ fun copy(): MediaHostState } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index 6f82d5dfff15..173a964cc5d3 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -80,6 +80,7 @@ import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shared.Flags; import com.android.systemui.shared.rotation.RotationPolicyUtil; import com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode; import com.android.systemui.shared.system.QuickStepContract; @@ -501,9 +502,11 @@ public final class NavBarHelper implements Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED, gestureDefault ? 1 : 0, mUserTracker.getUserId()) != 0; + boolean supportsSwipeGesture = QuickStepContract.isGesturalMode(mNavBarMode) + || (QuickStepContract.isLegacyMode(mNavBarMode) && Flags.threeButtonCornerSwipe()); mAssistantAvailable = assistantAvailableForUser && mAssistantTouchGestureEnabled - && QuickStepContract.isGesturalMode(mNavBarMode); + && supportsSwipeGesture; dispatchAssistantEventUpdate(mAssistantAvailable, mLongPressHomeEnabled); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 0f82e024afe6..6bd880d56bbb 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -78,6 +78,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.gestural.domain.GestureInteractor; +import com.android.systemui.navigationbar.gestural.domain.TaskMatcher; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.plugins.PluginListener; @@ -474,9 +475,14 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } else { String[] gestureBlockingActivities = resources.getStringArray(resId); for (String gestureBlockingActivity : gestureBlockingActivities) { - mGestureInteractor.addGestureBlockedActivity( - ComponentName.unflattenFromString(gestureBlockingActivity), - GestureInteractor.Scope.Local); + final ComponentName component = + ComponentName.unflattenFromString(gestureBlockingActivity); + + if (component != null) { + mGestureInteractor.addGestureBlockedMatcher( + new TaskMatcher.TopActivityComponent(component), + GestureInteractor.Scope.Local); + } } } } catch (NameNotFoundException e) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt index 8f35343626e8..c1f238a03be2 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt @@ -16,10 +16,9 @@ package com.android.systemui.navigationbar.gestural.data.respository -import android.content.ComponentName -import android.util.ArraySet import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.navigationbar.gestural.domain.TaskMatcher import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -28,36 +27,43 @@ import kotlinx.coroutines.withContext /** A repository for storing gesture related information */ interface GestureRepository { - /** A {@link StateFlow} tracking activities currently blocked from gestures. */ - val gestureBlockedActivities: StateFlow<Set<ComponentName>> + /** A {@link StateFlow} tracking matchers that can block gestures. */ + val gestureBlockedMatchers: StateFlow<Set<TaskMatcher>> - /** Adds an activity to be blocked from gestures. */ - suspend fun addGestureBlockedActivity(activity: ComponentName) + /** Adds a matcher to determine whether a gesture should be blocked. */ + suspend fun addGestureBlockedMatcher(matcher: TaskMatcher) - /** Removes an activity from being blocked from gestures. */ - suspend fun removeGestureBlockedActivity(activity: ComponentName) + /** Removes a matcher from blocking from gestures. */ + suspend fun removeGestureBlockedMatcher(matcher: TaskMatcher) } @SysUISingleton class GestureRepositoryImpl @Inject constructor(@Main private val mainDispatcher: CoroutineDispatcher) : GestureRepository { - private val _gestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(ArraySet()) + private val _gestureBlockedMatchers = MutableStateFlow<Set<TaskMatcher>>(emptySet()) - override val gestureBlockedActivities: StateFlow<Set<ComponentName>> - get() = _gestureBlockedActivities + override val gestureBlockedMatchers: StateFlow<Set<TaskMatcher>> + get() = _gestureBlockedMatchers - override suspend fun addGestureBlockedActivity(activity: ComponentName) = + override suspend fun addGestureBlockedMatcher(matcher: TaskMatcher) = withContext(mainDispatcher) { - _gestureBlockedActivities.emit( - _gestureBlockedActivities.value.toMutableSet().apply { add(activity) } - ) + val existingMatchers = _gestureBlockedMatchers.value + if (existingMatchers.contains(matcher)) { + return@withContext + } + + _gestureBlockedMatchers.value = existingMatchers.toMutableSet().apply { add(matcher) } } - override suspend fun removeGestureBlockedActivity(activity: ComponentName) = + override suspend fun removeGestureBlockedMatcher(matcher: TaskMatcher) = withContext(mainDispatcher) { - _gestureBlockedActivities.emit( - _gestureBlockedActivities.value.toMutableSet().apply { remove(activity) } - ) + val existingMatchers = _gestureBlockedMatchers.value + if (!existingMatchers.contains(matcher)) { + return@withContext + } + + _gestureBlockedMatchers.value = + existingMatchers.toMutableSet().apply { remove(matcher) } } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt index 61828783cdd9..96386e520d5a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.navigationbar.gestural.domain -import android.content.ComponentName import com.android.app.tracing.coroutines.flow.flowOn import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -25,7 +24,6 @@ import com.android.systemui.navigationbar.gestural.data.respository.GestureRepos import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.shared.system.TaskStackChangeListener import com.android.systemui.shared.system.TaskStackChangeListeners -import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.emitOnStart import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject @@ -60,7 +58,7 @@ constructor( Global } - private val _localGestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(setOf()) + private val _localGestureBlockedMatchers = MutableStateFlow<Set<TaskMatcher>>(setOf()) private val _topActivity = conflatedCallbackFlow { @@ -79,53 +77,47 @@ constructor( .mapLatest { getTopActivity() } .distinctUntilChanged() - private suspend fun getTopActivity(): ComponentName? = + private suspend fun getTopActivity(): TaskInfo? = withContext(backgroundCoroutineContext) { - val runningTask = activityManagerWrapper.runningTask - runningTask?.topActivity + activityManagerWrapper.runningTask?.let { TaskInfo(it.topActivity, it.activityType) } } val topActivityBlocked = combine( _topActivity, - gestureRepository.gestureBlockedActivities, - _localGestureBlockedActivities.asStateFlow() - ) { activity, global, local -> - activity != null && (global + local).contains(activity) + gestureRepository.gestureBlockedMatchers, + _localGestureBlockedMatchers.asStateFlow() + ) { runningTask, global, local -> + runningTask != null && (global + local).any { it.matches(runningTask) } } - /** - * Adds an {@link Activity} to be blocked based on component when the topmost, focused {@link - * Activity}. - */ - fun addGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) { + /** Adds an [TaskMatcher] to decide whether gestures should be blocked. */ + fun addGestureBlockedMatcher(matcher: TaskMatcher, gestureScope: Scope) { scope.launch { when (gestureScope) { Scope.Local -> { - _localGestureBlockedActivities.emit( - _localGestureBlockedActivities.value.toMutableSet().apply { add(activity) } + _localGestureBlockedMatchers.emit( + _localGestureBlockedMatchers.value.toMutableSet().apply { add(matcher) } ) } Scope.Global -> { - gestureRepository.addGestureBlockedActivity(activity) + gestureRepository.addGestureBlockedMatcher(matcher) } } } } - /** Removes an {@link Activity} from being blocked from gestures. */ - fun removeGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) { + /** Removes a gesture from deciding whether gestures should be blocked */ + fun removeGestureBlockedMatcher(matcher: TaskMatcher, gestureScope: Scope) { scope.launch { when (gestureScope) { Scope.Local -> { - _localGestureBlockedActivities.emit( - _localGestureBlockedActivities.value.toMutableSet().apply { - remove(activity) - } + _localGestureBlockedMatchers.emit( + _localGestureBlockedMatchers.value.toMutableSet().apply { remove(matcher) } ) } Scope.Global -> { - gestureRepository.removeGestureBlockedActivity(activity) + gestureRepository.removeGestureBlockedMatcher(matcher) } } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/TaskMatcher.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/TaskMatcher.kt new file mode 100644 index 000000000000..d62b2c001fca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/TaskMatcher.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.systemui.navigationbar.gestural.domain + +import android.content.ComponentName + +/** + * A simple data class for capturing details around a task. Implements equality to ensure changes + * can be identified between emitted values. + */ +data class TaskInfo(val topActivity: ComponentName?, val topActivityType: Int) { + override fun equals(other: Any?): Boolean { + return other is TaskInfo && + other.topActivityType == topActivityType && + other.topActivity == topActivity + } +} + +/** + * [TaskMatcher] provides a way to identify a task based on particular attributes, such as the top + * activity type or component name. + */ +sealed interface TaskMatcher { + fun matches(info: TaskInfo): Boolean + + class TopActivityType(private val type: Int) : TaskMatcher { + override fun matches(info: TaskInfo): Boolean { + return info.topActivity != null && info.topActivityType == type + } + } + + class TopActivityComponent(private val component: ComponentName) : TaskMatcher { + override fun matches(info: TaskInfo): Boolean { + return component == info.topActivity + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index c3274b7ca28b..c39ff557e54f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable @@ -403,30 +404,40 @@ constructor( onDispose { qqsVisible.value = false } } Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) { - QuickQuickSettings( - viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel, - modifier = - Modifier.onGloballyPositioned { coordinates -> - val (leftFromRoot, topFromRoot) = coordinates.positionInRoot().round() - val (width, height) = coordinates.size - qqsPositionOnRoot.set( - leftFromRoot, - topFromRoot, - leftFromRoot + width, - topFromRoot + height - ) - } - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - qqsHeight.value = placeable.height + Box(modifier = Modifier.fillMaxWidth()) { + val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle() + if (qsEnabled) { + QuickQuickSettings( + viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel, + modifier = + Modifier.onGloballyPositioned { coordinates -> + val (leftFromRoot, topFromRoot) = + coordinates.positionInRoot().round() + val (width, height) = coordinates.size + qqsPositionOnRoot.set( + leftFromRoot, + topFromRoot, + leftFromRoot + width, + topFromRoot + height + ) + } + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + qqsHeight.value = placeable.height - layout(placeable.width, placeable.height) { placeable.place(0, 0) } - } - .padding(top = { qqsPadding }) - .collapseExpandSemanticAction( - stringResource(id = R.string.accessibility_quick_settings_expand) - ) - ) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + .padding(top = { qqsPadding }) + .collapseExpandSemanticAction( + stringResource( + id = R.string.accessibility_quick_settings_expand + ) + ) + ) + } + } Spacer(modifier = Modifier.weight(1f)) } } @@ -441,18 +452,23 @@ constructor( stringResource(id = R.string.accessibility_quick_settings_collapse) ) ) { - Box(modifier = Modifier.fillMaxSize().weight(1f)) { - Column { - Spacer(modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() }) - ShadeBody(viewModel = viewModel.containerViewModel) + val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle() + if (qsEnabled) { + Box(modifier = Modifier.fillMaxSize().weight(1f)) { + Column { + Spacer( + modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() } + ) + ShadeBody(viewModel = viewModel.containerViewModel) + } + } + QuickSettingsTheme { + FooterActions( + viewModel = viewModel.footerActionsViewModel, + qsVisibilityLifecycleOwner = this@QSFragmentCompose, + modifier = Modifier.sysuiResTag("qs_footer_actions") + ) } - } - QuickSettingsTheme { - FooterActions( - viewModel = viewModel.footerActionsViewModel, - qsVisibilityLifecycleOwner = this@QSFragmentCompose, - modifier = Modifier.sysuiResTag("qs_footer_actions") - ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index df77878b88d9..16133f482f7b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -132,13 +132,17 @@ constructor( _stackScrollerOverscrolling.value = value } - private val qsDisabled = + /** + * Whether QS is enabled by policy. This is normally true, except when it's disabled by some + * policy. See [DisableFlagsRepository]. + */ + val qsEnabled = disableFlagsRepository.disableFlags - .map { !it.isQuickSettingsEnabled() } + .map { it.isQuickSettingsEnabled() } .stateIn( lifecycleScope, SharingStarted.WhileSubscribed(), - !disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled() + disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled() ) private val _showCollapsedOnKeyguard = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt index 12f3c9cd0b1b..93bf73fbfae5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneContentViewModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.qs.ui.viewmodel import androidx.lifecycle.LifecycleOwner -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel @@ -44,7 +43,7 @@ constructor( private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, private val footerActionsController: FooterActionsController, val mediaCarouselInteractor: MediaCarouselInteractor, -) : SysUiViewModel { +) { val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasAnyMediaOrRecommendation diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt index cb99be48912e..924a93923b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.ui.viewmodel -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,7 +34,7 @@ class QuickSettingsShadeSceneContentViewModel constructor( val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory, val quickSettingsContainerViewModel: QuickSettingsContainerViewModel, -) : SysUiViewModel { +) { @AssistedFactory interface Factory { diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt index ea61bd32c1f2..04620d6982d2 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt @@ -118,7 +118,7 @@ constructor( get() = when (this) { is ObservableTransitionState.Idle -> currentScene.canBeOccluded - is ObservableTransitionState.Transition.ChangeCurrentScene -> + is ObservableTransitionState.Transition.ChangeScene -> fromScene.canBeOccluded && toScene.canBeOccluded is ObservableTransitionState.Transition.ReplaceOverlay, is ObservableTransitionState.Transition.ShowOrHideOverlay -> diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index bdb148acbb37..a2142b6ce30c 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -131,7 +131,7 @@ constructor( .map { state -> when (state) { is ObservableTransitionState.Idle -> null - is ObservableTransitionState.Transition.ChangeCurrentScene -> state.toScene + is ObservableTransitionState.Transition.ChangeScene -> state.toScene is ObservableTransitionState.Transition.ShowOrHideOverlay, is ObservableTransitionState.Transition.ReplaceOverlay -> TODO("b/359173565: Handle overlay transitions") diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index cc46216b6e43..7eb48d6a06ff 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -479,7 +479,7 @@ constructor( switchToScene( targetSceneKey = Scenes.Lockscreen, loggingReason = "device is starting to sleep", - sceneState = keyguardTransitionInteractor.asleepKeyguardState.value, + sceneState = keyguardInteractor.asleepKeyguardState.value, ) } else { val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt index ec743ba5c91e..d1629c799732 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt @@ -112,7 +112,7 @@ constructor( // It // happens only when unlocking or when dismissing a dismissible lockscreen. val isTransitioningAwayFromKeyguard = - transitionState is ObservableTransitionState.Transition.ChangeCurrentScene && + transitionState is ObservableTransitionState.Transition.ChangeScene && transitionState.fromScene.isKeyguard() && transitionState.toScene == Scenes.Gone @@ -120,7 +120,7 @@ constructor( val isCurrentSceneShade = currentScene.isShade() // This is true when moving into one of the shade scenes when a non-shade scene. val isTransitioningToShade = - transitionState is ObservableTransitionState.Transition.ChangeCurrentScene && + transitionState is ObservableTransitionState.Transition.ChangeScene && !transitionState.fromScene.isShade() && transitionState.toScene.isShade() diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index b870a4e0b5e3..ec6513a99cad 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -104,6 +104,7 @@ object SceneWindowRootViewBinder { view.repeatWhenAttached { view.viewModel( + traceName = "SceneWindowRootViewBinder", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { viewModelFactory.create(motionEventHandlerReceiver) }, ) { viewModel -> diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt index 9144f16d9251..368e4fa06a26 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneActionsViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.scene.ui.viewmodel import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -33,7 +32,7 @@ import kotlinx.coroutines.flow.asStateFlow * need to worry about resetting the value of [actions] when the view-model is deactivated/canceled, * this base class takes care of it. */ -abstract class SceneActionsViewModel : SysUiViewModel, ExclusiveActivatable() { +abstract class SceneActionsViewModel : ExclusiveActivatable() { private val _actions = MutableStateFlow<Map<UserAction, UserActionResult>>(emptyMap()) /** diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index e2947d394d69..a73c39da256a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -26,7 +26,6 @@ import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.logger.SceneLogger @@ -47,14 +46,15 @@ constructor( private val powerInteractor: PowerInteractor, private val logger: SceneLogger, @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, -) : SysUiViewModel, ExclusiveActivatable() { +) : ExclusiveActivatable() { + /** The scene that should be rendered. */ val currentScene: StateFlow<SceneKey> = sceneInteractor.currentScene - private val hydrator = Hydrator() + private val hydrator = Hydrator("SceneContainerViewModel.hydrator") /** Whether the container is visible. */ - val isVisible: Boolean by hydrator.hydratedStateOf(sceneInteractor.isVisible) + val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible) override suspend fun onActivated(): Nothing { try { diff --git a/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java index cc470a647237..05f19ef2f043 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java +++ b/packages/SystemUI/src/com/android/systemui/settings/MultiUserUtilsModule.java @@ -67,7 +67,7 @@ public abstract class MultiUserUtilsModule { @Background CoroutineDispatcher backgroundDispatcher, @Background Handler handler ) { - int startingUser = userManager.getBootUser().getIdentifier(); + int startingUser = ActivityManager.getCurrentUser(); UserTrackerImpl tracker = new UserTrackerImpl(context, featureFlagsProvider, userManager, iActivityManager, dumpManager, appScope, backgroundDispatcher, handler); tracker.initialize(startingUser); diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt index 706797d9bbd2..52bc25dc5e23 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt @@ -20,7 +20,6 @@ import android.content.res.Resources import android.util.Log import android.view.View import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R import com.android.systemui.settings.brightness.BrightnessSliderController import com.android.systemui.settings.brightness.MirrorController @@ -37,7 +36,7 @@ constructor( private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor, @Main private val resources: Resources, val sliderControllerFactory: BrightnessSliderController.Factory, -) : SysUiViewModel, MirrorController { +) : MirrorController { private val tempPosition = IntArray(2) diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt index 7d6712166a21..e276f8807df7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt @@ -64,7 +64,7 @@ constructor( 0f } ) - is ObservableTransitionState.Transition.ChangeCurrentScene -> + is ObservableTransitionState.Transition.ChangeScene -> when { state.fromScene == Scenes.Gone -> if (state.toScene.isExpandable()) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt index f270e821840a..9c4bf1faf5cd 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationShadeWindowModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.util.kotlin.BooleanFlowOperators.any import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** Models UI state for the shade window. */ @SysUISingleton @@ -42,9 +43,11 @@ constructor( val isKeyguardOccluded: Flow<Boolean> = listOf( // Finished in state... - keyguardTransitionInteractor.isFinishedIn(OCCLUDED), - keyguardTransitionInteractor.isFinishedIn(DREAMING), - keyguardTransitionInteractor.isFinishedIn(Scenes.Communal, GLANCEABLE_HUB), + keyguardTransitionInteractor.transitionValue(OCCLUDED).map { it == 1f }, + keyguardTransitionInteractor.transitionValue(DREAMING).map { it == 1f }, + keyguardTransitionInteractor.transitionValue(Scenes.Communal, GLANCEABLE_HUB).map { + it == 1f + }, // ... or transitions between those states keyguardTransitionInteractor.isInTransition(Edge.create(OCCLUDED, DREAMING)), diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt index 25ae44ef8cfe..c210fc509cec 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.shade.ui.viewmodel import com.android.compose.animation.scene.SceneKey import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes @@ -37,8 +36,10 @@ import kotlinx.coroutines.flow.collectLatest */ class OverlayShadeViewModel @AssistedInject -constructor(private val sceneInteractor: SceneInteractor, shadeInteractor: ShadeInteractor) : - SysUiViewModel, ExclusiveActivatable() { +constructor( + private val sceneInteractor: SceneInteractor, + shadeInteractor: ShadeInteractor, +) : ExclusiveActivatable() { private val _backgroundScene = MutableStateFlow(Scenes.Lockscreen) /** The scene to show in the background when the overlay shade is open. */ val backgroundScene: StateFlow<SceneKey> = _backgroundScene.asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index edfe79ad91b8..c8abb1fe1dd5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -25,7 +25,6 @@ import android.os.UserHandle import android.provider.Settings import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.plugins.ActivityStarter import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyItem @@ -66,7 +65,7 @@ constructor( private val privacyChipInteractor: PrivacyChipInteractor, private val clockInteractor: ShadeHeaderClockInteractor, private val broadcastDispatcher: BroadcastDispatcher, -) : SysUiViewModel, ExclusiveActivatable() { +) : ExclusiveActivatable() { /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt index f0f2a65d9abb..7a79a22ebea2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt @@ -21,7 +21,6 @@ package com.android.systemui.shade.ui.viewmodel import androidx.lifecycle.LifecycleOwner import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.qs.FooterActionsController import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel @@ -62,7 +61,7 @@ constructor( private val unfoldTransitionInteractor: UnfoldTransitionInteractor, private val deviceEntryInteractor: DeviceEntryInteractor, private val sceneInteractor: SceneInteractor, -) : SysUiViewModel, ExclusiveActivatable() { +) : ExclusiveActivatable() { val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt index 173ff37cc3b8..be733d4e1e8e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt @@ -16,14 +16,24 @@ package com.android.systemui.statusbar.chips +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory +import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModel +import dagger.Binds import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module abstract class StatusBarChipsModule { + @Binds + @IntoMap + @ClassKey(DemoRonChipViewModel::class) + abstract fun binds(impl: DemoRonChipViewModel): CoreStartable + companion object { @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index 18ea0b445481..e82525810c64 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -69,7 +69,7 @@ constructor( state.notificationIconView ) } else { - OngoingActivityChipModel.ChipIcon.Basic(phoneIcon) + OngoingActivityChipModel.ChipIcon.SingleColorIcon(phoneIcon) } // This block mimics OngoingCallController#updateChip. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index cf4e7072a7d1..d4ad6ee0d04b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -190,7 +190,7 @@ constructor( ): OngoingActivityChipModel.Shown { return OngoingActivityChipModel.Shown.Timer( icon = - OngoingActivityChipModel.ChipIcon.Basic( + OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( CAST_TO_OTHER_DEVICE_ICON, // This string is "Casting screen" @@ -215,7 +215,7 @@ constructor( private fun createIconOnlyCastChip(deviceName: String?): OngoingActivityChipModel.Shown { return OngoingActivityChipModel.Shown.IconOnly( icon = - OngoingActivityChipModel.ChipIcon.Basic( + OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( CAST_TO_OTHER_DEVICE_ICON, // This string is just "Casting" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt new file mode 100644 index 000000000000..84ccaec62867 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModel.kt @@ -0,0 +1,158 @@ +/* + * 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.statusbar.chips.ron.demo.ui.viewmodel + +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.graphics.drawable.Drawable +import com.android.systemui.CoreStartable +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips +import com.android.systemui.statusbar.chips.ui.model.ColorsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.statusbar.commandline.ParseableCommand +import com.android.systemui.statusbar.commandline.Type +import com.android.systemui.util.time.SystemClock +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * A view model that will emit demo RON chips (rich ongoing notification chips) from [chip] based on + * adb commands sent by the user. + * + * Example adb commands: + * + * To show a chip with the SysUI icon and custom text: + * ``` + * adb shell cmd statusbar demo-ron -p com.android.systemui -t 10min + * ``` + * + * To hide the chip: + * ``` + * adb shell cmd statusbar demo-ron --hide + * ``` + * + * See [DemoRonCommand] for more information on the adb command spec. + */ +@SysUISingleton +class DemoRonChipViewModel +@Inject +constructor( + private val commandRegistry: CommandRegistry, + private val packageManager: PackageManager, + private val systemClock: SystemClock, +) : OngoingActivityChipViewModel, CoreStartable { + override fun start() { + commandRegistry.registerCommand("demo-ron") { DemoRonCommand() } + } + + private val _chip = + MutableStateFlow<OngoingActivityChipModel>(OngoingActivityChipModel.Hidden()) + override val chip: StateFlow<OngoingActivityChipModel> = _chip.asStateFlow() + + private inner class DemoRonCommand : ParseableCommand("demo-ron") { + private val packageName: String? by + param( + longName = "packageName", + shortName = "p", + description = "The package name for the demo RON app", + valueParser = Type.String, + ) + + private val text: String? by + param( + longName = "text", + shortName = "t", + description = "Text to display in the chip", + valueParser = Type.String, + ) + + private val hide by + flag( + longName = "hide", + description = "Hides any existing demo RON chip", + ) + + override fun execute(pw: PrintWriter) { + if (!StatusBarRonChips.isEnabled) { + pw.println( + "Error: com.android.systemui.status_bar_ron_chips must be enabled " + + "before using this demo feature" + ) + return + } + + if (hide) { + _chip.value = OngoingActivityChipModel.Hidden() + return + } + + val currentPackageName = packageName + if (currentPackageName == null) { + pw.println("--packageName (or -p) must be included") + return + } + + val appIcon = getAppIcon(currentPackageName) + if (appIcon == null) { + pw.println("Package $currentPackageName could not be found") + return + } + + val currentText = text + if (currentText != null) { + _chip.value = + OngoingActivityChipModel.Shown.Text( + icon = appIcon, + // TODO(b/361346412): Include a demo with a custom color theme. + colors = ColorsModel.Themed, + text = currentText, + ) + } else { + _chip.value = + OngoingActivityChipModel.Shown.Timer( + icon = appIcon, + // TODO(b/361346412): Include a demo with a custom color theme. + colors = ColorsModel.Themed, + startTimeMs = systemClock.elapsedRealtime(), + onClickListener = null, + ) + } + } + + private fun getAppIcon(packageName: String): OngoingActivityChipModel.ChipIcon? { + lateinit var iconDrawable: Drawable + try { + // Note: For the real implementation, we should check if applicationInfo exists + // before fetching the icon, so that we either don't show the chip or show a good + // backup icon in case the app info can't be found for some reason. + iconDrawable = packageManager.getApplicationIcon(packageName) + } catch (e: NameNotFoundException) { + return null + } + return OngoingActivityChipModel.ChipIcon.FullColorAppIcon( + Icon.Loaded(drawable = iconDrawable, contentDescription = null), + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.kt new file mode 100644 index 000000000000..4ef190991f19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ron/shared/StatusBarRonChips.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.statusbar.chips.ron.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the status bar RON chips flag state. */ +@Suppress("NOTHING_TO_INLINE") +object StatusBarRonChips { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_RON_CHIPS + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.statusBarRonChips() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is not enabled to ensure that the refactor author catches issues in testing. + * Caution!! Using this check incorrectly will cause crashes in nextfood builds! + */ + @JvmStatic + inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index 9e6cacb8b9ff..eb735211a970 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -80,7 +80,7 @@ constructor( is ScreenRecordChipModel.Recording -> { OngoingActivityChipModel.Shown.Timer( icon = - OngoingActivityChipModel.ChipIcon.Basic( + OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( ICON, ContentDescription.Resource( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index 7897f93b6496..d99a916b78a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -110,7 +110,7 @@ constructor( ): OngoingActivityChipModel.Shown { return OngoingActivityChipModel.Shown.Timer( icon = - OngoingActivityChipModel.ChipIcon.Basic( + OngoingActivityChipModel.ChipIcon.SingleColorIcon( Icon.Resource( SHARE_TO_APP_ICON, ContentDescription.Resource(R.string.share_to_app_chip_accessibility_label), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index 26a2f9139608..62622a893ec5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -89,6 +89,16 @@ sealed class OngoingActivityChipModel { ) : Shown(icon = null, colors, onClickListener = null) { override val logName = "Shown.Countdown" } + + /** This chip shows the specified [text] in the chip. */ + data class Text( + override val icon: ChipIcon, + override val colors: ColorsModel, + // TODO(b/361346412): Enforce a max length requirement? + val text: String, + ) : Shown(icon, colors, onClickListener = null) { + override val logName = "Shown.Text" + } } /** Represents an icon to show on the chip. */ @@ -106,7 +116,13 @@ sealed class OngoingActivityChipModel { } } - /** The icon is a basic resource or drawable icon that System UI created internally. */ - data class Basic(val impl: Icon) : ChipIcon + /** + * This icon is a single color and it came from basic resource or drawable icon that System + * UI created internally. + */ + data class SingleColorIcon(val impl: Icon) : ChipIcon + + /** This icon is an app icon in full color (so it should not get tinted in any way). */ + data class FullColorAppIcon(val impl: Icon) : ChipIcon } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index b0d897def53f..04c4516c9bed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -23,6 +23,8 @@ import com.android.systemui.log.core.LogLevel import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel +import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModel +import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel @@ -51,6 +53,7 @@ constructor( shareToAppChipViewModel: ShareToAppChipViewModel, castToOtherDeviceChipViewModel: CastToOtherDeviceChipViewModel, callChipViewModel: CallChipViewModel, + demoRonChipViewModel: DemoRonChipViewModel, @StatusBarChipsLog private val logger: LogBuffer, ) { private enum class ChipType { @@ -58,6 +61,8 @@ constructor( ShareToApp, CastToOtherDevice, Call, + /** A demo of a RON chip (rich ongoing notification chip), used just for testing. */ + DemoRon, } /** Model that helps us internally track the various chip states from each of the types. */ @@ -78,6 +83,7 @@ constructor( val shareToApp: OngoingActivityChipModel.Hidden, val castToOtherDevice: OngoingActivityChipModel.Hidden, val call: OngoingActivityChipModel.Hidden, + val demoRon: OngoingActivityChipModel.Hidden, ) : InternalChipModel } @@ -87,7 +93,8 @@ constructor( shareToAppChipViewModel.chip, castToOtherDeviceChipViewModel.chip, callChipViewModel.chip, - ) { screenRecord, shareToApp, castToOtherDevice, call -> + demoRonChipViewModel.chip, + ) { screenRecord, shareToApp, castToOtherDevice, call, demoRon -> logger.log( TAG, LogLevel.INFO, @@ -98,7 +105,15 @@ constructor( }, { "Chips: ScreenRecord=$str1 > ShareToApp=$str2 > CastToOther=$str3..." }, ) - logger.log(TAG, LogLevel.INFO, { str1 = call.logName }, { "... > Call=$str1" }) + logger.log( + TAG, + LogLevel.INFO, + { + str1 = call.logName + str2 = demoRon.logName + }, + { "... > Call=$str1 > DemoRon=$str2" } + ) // This `when` statement shows the priority order of the chips. when { // Screen recording also activates the media projection APIs, so whenever the @@ -113,17 +128,23 @@ constructor( InternalChipModel.Shown(ChipType.CastToOtherDevice, castToOtherDevice) call is OngoingActivityChipModel.Shown -> InternalChipModel.Shown(ChipType.Call, call) + demoRon is OngoingActivityChipModel.Shown -> { + StatusBarRonChips.assertInNewMode() + InternalChipModel.Shown(ChipType.DemoRon, demoRon) + } else -> { // We should only get here if all chip types are hidden check(screenRecord is OngoingActivityChipModel.Hidden) check(shareToApp is OngoingActivityChipModel.Hidden) check(castToOtherDevice is OngoingActivityChipModel.Hidden) check(call is OngoingActivityChipModel.Hidden) + check(demoRon is OngoingActivityChipModel.Hidden) InternalChipModel.Hidden( screenRecord = screenRecord, shareToApp = shareToApp, castToOtherDevice = castToOtherDevice, call = call, + demoRon = demoRon, ) } } @@ -154,6 +175,7 @@ constructor( ChipType.ShareToApp -> new.shareToApp ChipType.CastToOtherDevice -> new.castToOtherDevice ChipType.Call -> new.call + ChipType.DemoRon -> new.demoRon } } else if (new is InternalChipModel.Shown) { // If we have a chip to show, always show it. @@ -179,6 +201,7 @@ constructor( shareToApp = OngoingActivityChipModel.Hidden(), castToOtherDevice = OngoingActivityChipModel.Hidden(), call = OngoingActivityChipModel.Hidden(), + demoRon = OngoingActivityChipModel.Hidden(), ) } } 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 669349a88827..1f767aa1cbbc 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 @@ -2315,6 +2315,7 @@ public class NotificationStackScrollLayout private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate, boolean isRubberbanded) { + SceneContainerFlag.assertInLegacyMode(); amount = Math.max(0, amount); if (animate) { mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 3cc6e81986c5..6d5553fec6b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -67,6 +67,7 @@ constructor( suspend fun bind(): Nothing = view.asView().viewModel( + traceName = "NotificationScrollViewBinder", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = viewModelFactory::create, ) { viewModel -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 3999578b83e3..3e42413932f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -19,12 +19,11 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ObservableTransitionState.Idle import com.android.compose.animation.scene.ObservableTransitionState.Transition -import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeCurrentScene +import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene import com.android.compose.animation.scene.SceneKey import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneFamilies @@ -63,7 +62,6 @@ constructor( keyguardInteractor: Lazy<KeyguardInteractor>, ) : ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"), - SysUiViewModel, ExclusiveActivatable() { override suspend fun onActivated(): Nothing { @@ -79,7 +77,7 @@ constructor( } } - private fun fullyExpandedDuringSceneChange(change: ChangeCurrentScene): Boolean { + private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean { // The lockscreen stack is visible during all transitions away from the lockscreen, so keep // the stack expanded until those transitions finish. return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) || @@ -87,7 +85,7 @@ constructor( } private fun expandFractionDuringSceneChange( - change: ChangeCurrentScene, + change: ChangeScene, shadeExpansion: Float, qsExpansion: Float, ): Float { @@ -120,7 +118,7 @@ constructor( ) { shadeExpansion, _, qsExpansion, transitionState, _ -> when (transitionState) { is Idle -> if (expandedInScene(transitionState.currentScene)) 1f else 0f - is ChangeCurrentScene -> + is ChangeScene -> expandFractionDuringSceneChange( transitionState, shadeExpansion, @@ -250,7 +248,7 @@ constructor( } } -private fun ChangeCurrentScene.isBetween( +private fun ChangeScene.isBetween( a: (SceneKey) -> Boolean, - b: (SceneKey) -> Boolean + b: (SceneKey) -> Boolean, ): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene)) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index d891f62b4563..69c1bf3b61b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -21,7 +21,6 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -53,7 +52,6 @@ constructor( featureFlags: FeatureFlagsClassic, dumpManager: DumpManager, ) : - SysUiViewModel, ExclusiveActivatable(), ActivatableFlowDumper by ActivatableFlowDumperImpl( dumpManager = dumpManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index f63ee7b9520d..aed00d8cd5be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import androidx.annotation.VisibleForTesting +import com.android.compose.animation.scene.SceneKey import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.dagger.SysUISingleton @@ -141,10 +142,6 @@ constructor( private val communalSceneInteractor: CommunalSceneInteractor, unfoldTransitionInteractor: UnfoldTransitionInteractor, ) : FlowDumperImpl(dumpManager) { - // TODO(b/349784682): Transform deprecated states for Flexiglass - private val statesForConstrainedNotifications: Set<KeyguardState> = - setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER) - private val statesForHiddenKeyguard: Set<KeyguardState> = setOf(GONE, OCCLUDED) /** * Is either shade/qs expanded? This intentionally does not use the [ShadeInteractor] version, @@ -217,14 +214,16 @@ constructor( /** If the user is visually on one of the unoccluded lockscreen states. */ val isOnLockscreen: Flow<Boolean> = - combine( - keyguardTransitionInteractor.finishedKeyguardState.map { - statesForConstrainedNotifications.contains(it) - }, + anyOf( + keyguardTransitionInteractor.isFinishedIn(AOD), + keyguardTransitionInteractor.isFinishedIn(DOZING), + keyguardTransitionInteractor.isFinishedIn(ALTERNATE_BOUNCER), + keyguardTransitionInteractor.isFinishedIn( + scene = Scenes.Bouncer, + stateWithoutSceneContainer = PRIMARY_BOUNCER + ), keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, - ) { constrainedNotificationState, transitioningToOrFromLockscreen -> - constrainedNotificationState || transitioningToOrFromLockscreen - } + ) .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, @@ -250,9 +249,10 @@ constructor( /** If the user is visually on the glanceable hub or transitioning to/from it */ private val isOnGlanceableHub: Flow<Boolean> = combine( - keyguardTransitionInteractor.finishedKeyguardState.map { state -> - state == GLANCEABLE_HUB - }, + keyguardTransitionInteractor.isFinishedIn( + scene = Scenes.Communal, + stateWithoutSceneContainer = GLANCEABLE_HUB + ), anyOf( keyguardTransitionInteractor.isInTransition( edge = Edge.create(to = Scenes.Communal), @@ -424,32 +424,19 @@ constructor( .onStart { emit(1f) } .dumpWhileCollecting("alphaForShadeAndQsExpansion") - private fun toFlowArray( - states: Set<KeyguardState>, - flow: (KeyguardState) -> Flow<Boolean> - ): Array<Flow<Boolean>> { - return states.map { flow(it) }.toTypedArray() - } - private val isTransitioningToHiddenKeyguard: Flow<Boolean> = flow { while (currentCoroutineContext().isActive) { emit(false) // Ensure states are inactive to start - allOf( - *toFlowArray(statesForHiddenKeyguard) { state -> - keyguardTransitionInteractor.transitionValue(state).map { it == 0f } - } - ) - .first { it } + allOf(isNotOnState(OCCLUDED), isNotOnState(GONE, Scenes.Gone)).first { it } // Wait for a qualifying transition to begin anyOf( - *toFlowArray(statesForHiddenKeyguard) { state -> - keyguardTransitionInteractor - .transition(Edge.create(to = state)) - .map { it.value > 0f && it.transitionState == RUNNING } - .onStart { emit(false) } - } + transitionToIsRunning(Edge.create(to = OCCLUDED)), + transitionToIsRunning( + edge = Edge.create(to = Scenes.Gone), + edgeWithoutSceneContainer = Edge.create(to = GONE) + ) ) .first { it } emit(true) @@ -458,13 +445,7 @@ constructor( // it is considered safe to reset alpha to 1f for HUNs. combine( keyguardInteractor.statusBarState, - allOf( - *toFlowArray(statesForHiddenKeyguard) { state -> - keyguardTransitionInteractor.transitionValue(state).map { - it == 0f - } - } - ) + allOf(isNotOnState(OCCLUDED), isNotOnState(GONE, Scenes.Gone)) ) { statusBarState, stateIsReversed -> statusBarState == SHADE || stateIsReversed } @@ -473,6 +454,17 @@ constructor( } .dumpWhileCollecting("isTransitioningToHiddenKeyguard") + private fun isNotOnState(stateWithoutSceneContainer: KeyguardState, scene: SceneKey? = null) = + keyguardTransitionInteractor + .transitionValue(scene = scene, stateWithoutSceneContainer = stateWithoutSceneContainer) + .map { it == 0f } + + private fun transitionToIsRunning(edge: Edge, edgeWithoutSceneContainer: Edge? = null) = + keyguardTransitionInteractor + .transition(edge = edge, edgeWithoutSceneContainer = edgeWithoutSceneContainer) + .map { it.value > 0f && it.transitionState == RUNNING } + .onStart { emit(false) } + val panelAlpha = keyguardInteractor.panelAlpha private fun bouncerToGoneNotificationAlpha(viewState: ViewStateAccessor): Flow<Float> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt index d46aaf45b1a3..c24d69465043 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt @@ -35,6 +35,7 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.chips.ron.shared.StatusBarRonChips import com.android.systemui.statusbar.chips.ui.binder.ChipChronometerBinder import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer @@ -124,10 +125,6 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // Colors val textColor = chipModel.colors.text(chipContext) - chipDefaultIconView.imageTintList = - ColorStateList.valueOf(textColor) - chipBackgroundView.getCustomIconView()?.imageTintList = - ColorStateList.valueOf(textColor) chipTimeView.setTextColor(textColor) chipTextView.setTextColor(textColor) (chipBackgroundView.background as GradientDrawable).color = @@ -173,13 +170,22 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // it. backgroundView.removeView(backgroundView.getCustomIconView()) + val iconTint = chipModel.colors.text(defaultIconView.context) + when (val icon = chipModel.icon) { null -> { defaultIconView.visibility = View.GONE } - is OngoingActivityChipModel.ChipIcon.Basic -> { + is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> { + IconViewBinder.bind(icon.impl, defaultIconView) + defaultIconView.visibility = View.VISIBLE + defaultIconView.tintView(iconTint) + } + is OngoingActivityChipModel.ChipIcon.FullColorAppIcon -> { + StatusBarRonChips.assertInNewMode() IconViewBinder.bind(icon.impl, defaultIconView) defaultIconView.visibility = View.VISIBLE + defaultIconView.untintView() } is OngoingActivityChipModel.ChipIcon.StatusBarView -> { // Hide the default icon since we'll show this custom icon instead. @@ -194,6 +200,7 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa // maybe include the app name. contentDescription = context.resources.getString(R.string.ongoing_phone_call_content_description) + tintView(iconTint) } // 2. If we just reinflated the view, we may need to detach the icon view from the @@ -219,6 +226,14 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa return this.findViewById(CUSTOM_ICON_VIEW_ID) } + private fun ImageView.tintView(color: Int) { + this.imageTintList = ColorStateList.valueOf(color) + } + + private fun ImageView.untintView() { + this.imageTintList = null + } + private fun generateCustomIconLayoutParams(iconView: ImageView): FrameLayout.LayoutParams { val customIconSize = iconView.context.resources.getDimensionPixelSize( @@ -237,10 +252,13 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa chipTextView.text = chipModel.secondsUntilStarted.toString() chipTextView.visibility = View.VISIBLE - // The Chronometer should be stopped to prevent leaks -- see b/192243808 and - // [Chronometer.start]. - chipTimeView.stop() - chipTimeView.visibility = View.GONE + chipTimeView.hide() + } + is OngoingActivityChipModel.Shown.Text -> { + chipTextView.text = chipModel.text + chipTextView.visibility = View.VISIBLE + + chipTimeView.hide() } is OngoingActivityChipModel.Shown.Timer -> { ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView) @@ -250,14 +268,18 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa } is OngoingActivityChipModel.Shown.IconOnly -> { chipTextView.visibility = View.GONE - // The Chronometer should be stopped to prevent leaks -- see b/192243808 and - // [Chronometer.start]. - chipTimeView.stop() - chipTimeView.visibility = View.GONE + chipTimeView.hide() } } } + private fun ChipChronometer.hide() { + // The Chronometer should be stopped to prevent leaks -- see b/192243808 and + // [Chronometer.start]. + this.stop() + this.visibility = View.GONE + } + private fun updateChipPadding( chipModel: OngoingActivityChipModel.Shown, backgroundView: View, @@ -356,6 +378,7 @@ class CollapsedStatusBarViewBinderImpl @Inject constructor() : CollapsedStatusBa chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE } is OngoingActivityChipModel.Shown.Timer, + is OngoingActivityChipModel.Shown.Text, is OngoingActivityChipModel.Shown.IconOnly -> { chipView.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_NONE } diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index c7fc44513473..9c8ef0421888 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -89,6 +89,9 @@ import com.android.systemui.util.settings.SecureSettings; import com.google.ux.material.libmonet.dynamiccolor.DynamicColor; import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.StateFlow; + import org.json.JSONException; import org.json.JSONObject; @@ -162,6 +165,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final WakefulnessLifecycle mWakefulnessLifecycle; private final JavaAdapter mJavaAdapter; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private final StateFlow<Boolean> mIsKeyguardOnAsleepState; private final UiModeManager mUiModeManager; private ColorScheme mDarkColorScheme; private ColorScheme mLightColorScheme; @@ -202,8 +206,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { } boolean currentUser = userId == mUserTracker.getUserId(); boolean isAsleep = themeOverlayControllerWakefulnessDeprecation() - ? KeyguardState.Companion.deviceIsAsleepInState( - mKeyguardTransitionInteractor.getFinishedState()) + ? ThemeOverlayController.this.mIsKeyguardOnAsleepState.getValue() : mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP; if (currentUser && !mAcceptColorEvents && isAsleep) { @@ -434,6 +437,10 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mUiModeManager = uiModeManager; mActivityManager = activityManager; dumpManager.registerDumpable(TAG, this); + + Flow<Boolean> isFinishedInAsleepStateFlow = mKeyguardTransitionInteractor + .isFinishedInStateWhere(KeyguardState.Companion::deviceIsAsleepInState); + mIsKeyguardOnAsleepState = mJavaAdapter.stateInApp(isFinishedInAsleepStateFlow, false); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt index 727e51fdef11..315a89b96c25 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt @@ -20,7 +20,6 @@ import android.util.IndentingPrintWriter import com.android.systemui.Dumpable import com.android.systemui.dump.DumpManager import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.util.asIndenting import com.android.systemui.util.printCollection import java.io.PrintWriter diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 055671cf32ca..64e056da97d4 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -31,7 +31,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** A class allowing Java classes to collect on Kotlin flows. */ @@ -58,6 +61,15 @@ constructor( ): Job { return scope.launch { flow.collect { consumer.accept(it) } } } + + @JvmOverloads + fun <T> stateInApp( + flow: Flow<T>, + initialValue: T, + started: SharingStarted = SharingStarted.Eagerly + ): StateFlow<T> { + return flow.stateIn(scope, started, initialValue) + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index d3e8bd3d8b5b..28effe909521 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -125,7 +125,6 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa static final ArrayMap<Integer, Integer> STREAMS = new ArrayMap<>(); static { STREAMS.put(AudioSystem.STREAM_ALARM, R.string.stream_alarm); - STREAMS.put(AudioSystem.STREAM_BLUETOOTH_SCO, R.string.stream_bluetooth_sco); STREAMS.put(AudioSystem.STREAM_DTMF, R.string.stream_dtmf); STREAMS.put(AudioSystem.STREAM_MUSIC, R.string.stream_music); STREAMS.put(AudioSystem.STREAM_ACCESSIBILITY, R.string.stream_accessibility); @@ -654,7 +653,6 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa private static boolean isLogWorthy(int stream) { switch (stream) { case AudioSystem.STREAM_ALARM: - case AudioSystem.STREAM_BLUETOOTH_SCO: case AudioSystem.STREAM_MUSIC: case AudioSystem.STREAM_RING: case AudioSystem.STREAM_SYSTEM: diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index eb9151868b79..7786453814e0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -704,8 +704,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, addRow(AudioManager.STREAM_VOICE_CALL, com.android.internal.R.drawable.ic_phone, com.android.internal.R.drawable.ic_phone, false, false); - addRow(AudioManager.STREAM_BLUETOOTH_SCO, - R.drawable.ic_volume_bt_sco, R.drawable.ic_volume_bt_sco, false, false); addRow(AudioManager.STREAM_SYSTEM, R.drawable.ic_volume_system, R.drawable.ic_volume_system_mute, false, false); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt index 0451ce6d9fce..4be680ef66f1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractor.kt @@ -16,9 +16,7 @@ package com.android.systemui.volume.panel.component.volume.domain.interactor -import android.media.AudioDeviceInfo import android.media.AudioManager -import com.android.settingslib.volume.data.repository.AudioRepository import com.android.settingslib.volume.domain.interactor.AudioModeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor @@ -42,7 +40,6 @@ class AudioSlidersInteractor constructor( @VolumePanelScope scope: CoroutineScope, mediaOutputInteractor: MediaOutputInteractor, - audioRepository: AudioRepository, audioModeInteractor: AudioModeInteractor, ) { @@ -50,13 +47,12 @@ constructor( combineTransform( mediaOutputInteractor.activeMediaDeviceSessions, mediaOutputInteractor.defaultActiveMediaSession.filterData(), - audioRepository.communicationDevice, audioModeInteractor.isOngoingCall, - ) { activeSessions, defaultSession, communicationDevice, isOngoingCall -> + ) { activeSessions, defaultSession, isOngoingCall -> coroutineScope { val viewModels = buildList { if (isOngoingCall) { - addCall(communicationDevice?.type) + addStream(AudioManager.STREAM_VOICE_CALL) } if (defaultSession?.isTheSameSession(activeSessions.remote) == true) { @@ -68,7 +64,7 @@ constructor( } if (!isOngoingCall) { - addCall(communicationDevice?.type) + addStream(AudioManager.STREAM_VOICE_CALL) } addStream(AudioManager.STREAM_RING) @@ -80,14 +76,6 @@ constructor( } .stateIn(scope, SharingStarted.Eagerly, emptyList()) - private fun MutableList<SliderType>.addCall(communicationDeviceType: Int?) { - if (communicationDeviceType == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { - addStream(AudioManager.STREAM_BLUETOOTH_SCO) - } else { - addStream(AudioManager.STREAM_VOICE_CALL) - } - } - private fun MutableList<SliderType>.addSession(remoteMediaDeviceSession: MediaDeviceSession?) { if (remoteMediaDeviceSession?.canAdjustVolume == true) { add(SliderType.MediaDeviceCast(remoteMediaDeviceSession)) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 521f608878cb..ffb1f11c4970 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -66,7 +66,6 @@ constructor( mapOf( AudioStream(AudioManager.STREAM_MUSIC) to R.drawable.ic_music_note, AudioStream(AudioManager.STREAM_VOICE_CALL) to R.drawable.ic_call, - AudioStream(AudioManager.STREAM_BLUETOOTH_SCO) to R.drawable.ic_call, AudioStream(AudioManager.STREAM_RING) to R.drawable.ic_ring_volume, AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_ringer, AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_alarm, @@ -75,7 +74,6 @@ constructor( mapOf( AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_music, AudioStream(AudioManager.STREAM_VOICE_CALL) to R.string.stream_voice_call, - AudioStream(AudioManager.STREAM_BLUETOOTH_SCO) to R.string.stream_voice_call, AudioStream(AudioManager.STREAM_RING) to R.string.stream_ring, AudioStream(AudioManager.STREAM_NOTIFICATION) to R.string.stream_notification, AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm, @@ -91,8 +89,6 @@ constructor( VolumePanelUiEvent.VOLUME_PANEL_MUSIC_SLIDER_TOUCHED, AudioStream(AudioManager.STREAM_VOICE_CALL) to VolumePanelUiEvent.VOLUME_PANEL_VOICE_CALL_SLIDER_TOUCHED, - AudioStream(AudioManager.STREAM_BLUETOOTH_SCO) to - VolumePanelUiEvent.VOLUME_PANEL_VOICE_CALL_SLIDER_TOUCHED, AudioStream(AudioManager.STREAM_RING) to VolumePanelUiEvent.VOLUME_PANEL_RING_SLIDER_TOUCHED, AudioStream(AudioManager.STREAM_NOTIFICATION) to diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 74bc9282eebb..681ea754e630 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -59,6 +59,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { private lateinit var mockitoSession: StaticMockitoSession private lateinit var activeMediaDeviceItem: DeviceItem private lateinit var notConnectedDeviceItem: DeviceItem + private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem private lateinit var connectedMediaDeviceItem: DeviceItem private lateinit var connectedOtherDeviceItem: DeviceItem @Mock private lateinit var dialog: SystemUIDialog @@ -100,6 +101,15 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { iconWithDescription = null, background = null ) + connectedAudioSharingMediaDeviceItem = + DeviceItem( + type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null + ) connectedOtherDeviceItem = DeviceItem( type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, @@ -186,6 +196,21 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { } @Test + fun testOnClick_connectedAudioSharingMediaDevice_logClick() { + with(kosmos) { + testScope.runTest { + whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) + actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog) + verify(bluetoothTileDialogLogger) + .logDeviceClick( + cachedBluetoothDevice.address, + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE + ) + } + } + } + + @Test fun testOnClick_audioSharingDisabled_shouldNotLaunchSettings() { with(kosmos) { testScope.runTest { @@ -415,7 +440,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { } @Test - fun testOnClick_hasTwoConnectedLeDevice_clickedConnectedLe_shouldLaunchSettings() { + fun testOnClick_hasTwoConnectedLeDevice_clickedActiveLe_shouldLaunchSettings() { with(kosmos) { testScope.runTest { whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) @@ -438,7 +463,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2 } - actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) + actionInteractorImpl.onClick(activeMediaDeviceItem, dialog) verify(activityStarter) .postStartActivityDismissingKeyguard( ArgumentMatchers.any(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt index a27ccc67d584..ef441c1dc12c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt @@ -17,18 +17,24 @@ package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothDevice -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager +import android.graphics.drawable.Drawable import android.media.AudioManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper +import android.util.Pair import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.flags.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -37,6 +43,7 @@ import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any @SmallTest @RunWith(AndroidJUnit4::class) @@ -44,9 +51,11 @@ import org.mockito.junit.MockitoRule class DeviceItemFactoryTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private lateinit var mockitoSession: StaticMockitoSession @Mock private lateinit var cachedDevice: CachedBluetoothDevice @Mock private lateinit var bluetoothDevice: BluetoothDevice - @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var drawable: Drawable private val availableMediaDeviceItemFactory = AvailableMediaDeviceItemFactory() private val connectedDeviceItemFactory = ConnectedDeviceItemFactory() @@ -56,16 +65,21 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Before fun setup() { - `when`(cachedDevice.name).thenReturn(DEVICE_NAME) - `when`(cachedDevice.address).thenReturn(DEVICE_ADDRESS) - `when`(cachedDevice.device).thenReturn(bluetoothDevice) - `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY) + mockitoSession = + mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() + } - context.setMockPackageManager(packageManager) + @After + fun tearDown() { + mockitoSession.finishMocking() } @Test fun testAvailableMediaDeviceItemFactory_createFromCachedDevice() { + `when`(cachedDevice.name).thenReturn(DEVICE_NAME) + `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY) + `when`(BluetoothUtils.getBtClassDrawableWithDescription(any(), any())) + .thenReturn(Pair.create(drawable, "")) val deviceItem = availableMediaDeviceItemFactory.create(context, cachedDevice) assertDeviceItem(deviceItem, DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) @@ -73,6 +87,10 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testConnectedDeviceItemFactory_createFromCachedDevice() { + `when`(cachedDevice.name).thenReturn(DEVICE_NAME) + `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY) + `when`(BluetoothUtils.getBtClassDrawableWithDescription(any(), any())) + .thenReturn(Pair.create(drawable, "")) val deviceItem = connectedDeviceItemFactory.create(context, cachedDevice) assertDeviceItem(deviceItem, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) @@ -80,6 +98,10 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test fun testSavedDeviceItemFactory_createFromCachedDevice() { + `when`(cachedDevice.name).thenReturn(DEVICE_NAME) + `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY) + `when`(BluetoothUtils.getBtClassDrawableWithDescription(any(), any())) + .thenReturn(Pair.create(drawable, "")) val deviceItem = savedDeviceItemFactory.create(context, cachedDevice) assertDeviceItem(deviceItem, DeviceItemType.SAVED_BLUETOOTH_DEVICE) @@ -87,6 +109,90 @@ class DeviceItemFactoryTest : SysuiTestCase() { } @Test + fun testAvailableAudioSharingMediaDeviceItemFactory_createFromCachedDevice() { + `when`(cachedDevice.name).thenReturn(DEVICE_NAME) + `when`(BluetoothUtils.getBtClassDrawableWithDescription(any(), any())) + .thenReturn(Pair.create(drawable, "")) + val deviceItem = + AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager) + .create(context, cachedDevice) + + assertThat(deviceItem).isNotNull() + assertThat(deviceItem.type) + .isEqualTo(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) + assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice) + assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME) + assertThat(deviceItem.isActive).isFalse() + assertThat(deviceItem.connectionSummary) + .isEqualTo( + context.getString( + R.string.quick_settings_bluetooth_device_audio_sharing_or_switch_active + ) + ) + } + + @Test + fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_flagOff_returnsFalse() { + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast + // source or assistant. + `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) + + assertThat( + AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager) + .isFilterMatched(context, cachedDevice, audioManager) + ) + .isFalse() + } + + @Test + fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_returnsFalse() { + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and + // assistant. + `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) + `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(true) + + assertThat( + AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager) + .isFilterMatched(context, cachedDevice, audioManager) + ) + .isFalse() + } + + @Test + fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_returnsFalse() { + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and + // assistant. + `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) + `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false) + `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true) + `when`(BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(any(), any())) + .thenReturn(false) + + assertThat( + AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager) + .isFilterMatched(context, cachedDevice, audioManager) + ) + .isFalse() + } + + @Test + fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_returnsTrue() { + // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and + // assistant. + `when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) + `when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false) + `when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true) + `when`(BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(any(), any())) + .thenReturn(true) + + assertThat( + AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager) + .isFilterMatched(context, cachedDevice, audioManager) + ) + .isTrue() + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testSavedFactory_isFilterMatched_bondedAndNotConnected_returnsTrue() { `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) @@ -110,7 +216,6 @@ class DeviceItemFactoryTest : SysuiTestCase() { @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testSavedFactory_isFilterMatched_notBonded_returnsFalse() { `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) - `when`(cachedDevice.isConnected).thenReturn(false) assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) .isFalse() @@ -119,12 +224,8 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testSavedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() { - `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) - .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray()) - `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0)) - .thenReturn(ApplicationInfo()) - `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(cachedDevice.isConnected).thenReturn(false) + `when`(cachedDevice.device).thenReturn(bluetoothDevice) + `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(true) assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) .isFalse() @@ -132,35 +233,9 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testSavedFactory_isFilterMatched_noExclusiveManager_returnsTrue() { - `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(cachedDevice.isConnected).thenReturn(false) - - assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) - .isTrue() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testSavedFactory_isFilterMatched_exclusiveManagerNotEnabled_returnsTrue() { - `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) - .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray()) - `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0)) - .thenReturn(ApplicationInfo().also { it.enabled = false }) - `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(cachedDevice.isConnected).thenReturn(false) - - assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) - .isTrue() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testSavedFactory_isFilterMatched_exclusiveManagerNotInstalled_returnsTrue() { - `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) - .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray()) - `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0)) - .thenThrow(PackageManager.NameNotFoundException("Test!")) + fun testSavedFactory_isFilterMatched_notExclusiveManaged_returnsTrue() { + `when`(cachedDevice.device).thenReturn(bluetoothDevice) + `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(false) `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) `when`(cachedDevice.isConnected).thenReturn(false) @@ -170,17 +245,9 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testSavedFactory_isFilterMatched_notExclusivelyManaged_notBonded_returnsFalse() { - `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) - `when`(cachedDevice.isConnected).thenReturn(false) - - assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) - .isFalse() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testSavedFactory_isFilterMatched_notExclusivelyManaged_connected_returnsFalse() { + `when`(cachedDevice.device).thenReturn(bluetoothDevice) + `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(false) `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) `when`(cachedDevice.isConnected).thenReturn(true) @@ -191,9 +258,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testConnectedFactory_isFilterMatched_bondedAndConnected_returnsTrue() { - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(bluetoothDevice.isConnected).thenReturn(true) - audioManager.setMode(AudioManager.MODE_NORMAL) + `when`(BluetoothUtils.isConnectedBluetoothDevice(any(), any())).thenReturn(true) assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) .isTrue() @@ -202,21 +267,6 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testConnectedFactory_isFilterMatched_notConnected_returnsFalse() { - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(bluetoothDevice.isConnected).thenReturn(false) - audioManager.setMode(AudioManager.MODE_NORMAL) - - assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) - .isFalse() - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testConnectedFactory_isFilterMatched_notBonded_returnsFalse() { - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) - `when`(bluetoothDevice.isConnected).thenReturn(true) - audioManager.setMode(AudioManager.MODE_NORMAL) - assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) .isFalse() } @@ -224,13 +274,8 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testConnectedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() { - `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) - .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray()) - `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0)) - .thenReturn(ApplicationInfo()) - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(bluetoothDevice.isConnected).thenReturn(true) - audioManager.setMode(AudioManager.MODE_NORMAL) + `when`(cachedDevice.device).thenReturn(bluetoothDevice) + `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(true) assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) .isFalse() @@ -239,39 +284,9 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testConnectedFactory_isFilterMatched_noExclusiveManager_returnsTrue() { - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(bluetoothDevice.isConnected).thenReturn(true) - audioManager.setMode(AudioManager.MODE_NORMAL) - - assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) - .isTrue() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testConnectedFactory_isFilterMatched_exclusiveManagerNotEnabled_returnsTrue() { - `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) - .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray()) - `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0)) - .thenReturn(ApplicationInfo().also { it.enabled = false }) - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(bluetoothDevice.isConnected).thenReturn(true) - audioManager.setMode(AudioManager.MODE_NORMAL) - - assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) - .isTrue() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testConnectedFactory_isFilterMatched_exclusiveManagerNotInstalled_returnsTrue() { - `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) - .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray()) - `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0)) - .thenThrow(PackageManager.NameNotFoundException("Test!")) - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(bluetoothDevice.isConnected).thenReturn(true) - audioManager.setMode(AudioManager.MODE_NORMAL) + `when`(cachedDevice.device).thenReturn(bluetoothDevice) + `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(false) + `when`(BluetoothUtils.isConnectedBluetoothDevice(any(), any())).thenReturn(true) assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) .isTrue() @@ -279,21 +294,10 @@ class DeviceItemFactoryTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) - fun testConnectedFactory_isFilterMatched_notExclusivelyManaged_notBonded_returnsFalse() { - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) - `when`(bluetoothDevice.isConnected).thenReturn(true) - audioManager.setMode(AudioManager.MODE_NORMAL) - - assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) - .isFalse() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) fun testConnectedFactory_isFilterMatched_notExclusivelyManaged_notConnected_returnsFalse() { - `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) - `when`(bluetoothDevice.isConnected).thenReturn(false) - audioManager.setMode(AudioManager.MODE_NORMAL) + `when`(cachedDevice.device).thenReturn(bluetoothDevice) + `when`(BluetoothUtils.isExclusivelyManagedBluetoothDevice(any(), any())).thenReturn(false) + `when`(BluetoothUtils.isConnectedBluetoothDevice(any(), any())).thenReturn(false) assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) .isFalse() @@ -310,7 +314,5 @@ class DeviceItemFactoryTest : SysuiTestCase() { companion object { const val DEVICE_NAME = "DeviceName" const val CONNECTION_SUMMARY = "ConnectionSummary" - private const val TEST_EXCLUSIVE_MANAGER = "com.test.manager" - private const val DEVICE_ADDRESS = "04:52:C7:0B:D8:3C" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt index 8e215f994e4d..fd550b05fdc9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt @@ -83,7 +83,9 @@ class BouncerContentTest : SysuiTestCase() { PlatformTheme { BouncerContent( viewModel = - rememberViewModel { kosmos.bouncerSceneContentViewModelFactory.create() }, + rememberViewModel("test") { + kosmos.bouncerSceneContentViewModelFactory.create() + }, layout = BouncerSceneLayout.BESIDE_USER_SWITCHER, modifier = Modifier.fillMaxSize().testTag("BouncerContent"), dialogFactory = bouncerDialogFactory diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt index 73b9f5783004..07f7557d965a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt @@ -47,8 +47,11 @@ import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepo import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +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.TransitionStep import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger @@ -56,7 +59,10 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R +import com.android.systemui.scene.data.repository.Idle +import com.android.systemui.scene.data.repository.setTransition import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -144,7 +150,6 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { @Mock private lateinit var glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel - @Mock private lateinit var transitionInteractor: KeyguardTransitionInteractor private val kosmos = testKosmos() @@ -163,8 +168,6 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { // the viewModel does a `map { 1 - it }` on this value, which is why it's different private val intendedShadeAlphaMutableStateFlow: MutableStateFlow<Float> = MutableStateFlow(0f) - private val intendedFinishedKeyguardStateFlow = MutableStateFlow(KeyguardState.LOCKSCREEN) - @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -256,7 +259,6 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { intendedAlphaMutableStateFlow.value = 1f intendedShadeAlphaMutableStateFlow.value = 0f - intendedFinishedKeyguardStateFlow.value = KeyguardState.LOCKSCREEN whenever(aodToLockscreenTransitionViewModel.shortcutsAlpha) .thenReturn(intendedAlphaMutableStateFlow) whenever(dozingToLockscreenTransitionViewModel.shortcutsAlpha).thenReturn(emptyFlow()) @@ -283,8 +285,6 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { whenever(glanceableHubToLockscreenTransitionViewModel.shortcutsAlpha) .thenReturn(emptyFlow()) whenever(shadeInteractor.anyExpansion).thenReturn(intendedShadeAlphaMutableStateFlow) - whenever(transitionInteractor.finishedKeyguardState) - .thenReturn(intendedFinishedKeyguardStateFlow) underTest = KeyguardQuickAffordancesCombinedViewModel( @@ -334,7 +334,7 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { lockscreenToPrimaryBouncerTransitionViewModel, lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel, - transitionInteractor = transitionInteractor, + transitionInteractor = kosmos.keyguardTransitionInteractor, ) } @@ -776,7 +776,10 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { @Test fun shadeExpansionAlpha_changes_whenOnLockscreen() = testScope.runTest { - intendedFinishedKeyguardStateFlow.value = KeyguardState.LOCKSCREEN + kosmos.setTransition( + sceneTransition = Idle(Scenes.Lockscreen), + stateTransition = TransitionStep(from = AOD, to = LOCKSCREEN) + ) intendedShadeAlphaMutableStateFlow.value = 0.25f val underTest = collectLastValue(underTest.transitionAlpha) assertEquals(0.75f, underTest()) @@ -788,7 +791,10 @@ class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() { @Test fun shadeExpansionAlpha_alwaysZero_whenNotOnLockscreen() = testScope.runTest { - intendedFinishedKeyguardStateFlow.value = KeyguardState.GONE + kosmos.setTransition( + sceneTransition = Idle(Scenes.Gone), + stateTransition = TransitionStep(from = AOD, to = GONE) + ) intendedShadeAlphaMutableStateFlow.value = 0.5f val underTest = collectLastValue(underTest.transitionAlpha) assertEquals(0f, underTest()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt index 67517a25ec87..2ba670ceb76a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt @@ -40,7 +40,7 @@ class ActivatableTest : SysuiTestCase() { composeRule.setContent { val keepAlive by keepAliveMutable if (keepAlive) { - rememberActivated { + rememberActivated("test") { FakeActivatable( onActivation = { isActive = true }, onDeactivation = { isActive = false }, @@ -58,7 +58,7 @@ class ActivatableTest : SysuiTestCase() { composeRule.setContent { val keepAlive by keepAliveMutable if (keepAlive) { - rememberActivated { + rememberActivated("name") { FakeActivatable( onActivation = { isActive = true }, onDeactivation = { isActive = false }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt index c7acd78c5623..73f724e7daef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt @@ -51,7 +51,7 @@ class SysUiViewModelTest : SysuiTestCase() { composeRule.setContent { val keepAlive by keepAliveMutable if (keepAlive) { - rememberViewModel { + rememberViewModel("test") { FakeSysUiViewModel( onActivation = { isActive = true }, onDeactivation = { isActive = false }, @@ -69,21 +69,25 @@ class SysUiViewModelTest : SysuiTestCase() { var isActive2 = false composeRule.setContent { val key by keyMutable - rememberViewModel(key) { - when (key) { - 1 -> - FakeSysUiViewModel( - onActivation = { isActive1 = true }, - onDeactivation = { isActive1 = false }, - ) - 2 -> - FakeSysUiViewModel( - onActivation = { isActive2 = true }, - onDeactivation = { isActive2 = false }, - ) - else -> error("unsupported key $key") + // Need to explicitly state the type to avoid a weird issue where the factory seems to + // return Unit instead of FakeSysUiViewModel. It might be an issue with the compose + // compiler. + val unused: FakeSysUiViewModel = + rememberViewModel("test", key) { + when (key) { + 1 -> + FakeSysUiViewModel( + onActivation = { isActive1 = true }, + onDeactivation = { isActive1 = false }, + ) + 2 -> + FakeSysUiViewModel( + onActivation = { isActive2 = true }, + onDeactivation = { isActive2 = false }, + ) + else -> error("unsupported key $key") + } } - } } assertThat(isActive1).isTrue() assertThat(isActive2).isFalse() @@ -106,7 +110,7 @@ class SysUiViewModelTest : SysuiTestCase() { composeRule.setContent { val keepAlive by keepAliveMutable if (keepAlive) { - rememberViewModel { + rememberViewModel("test") { FakeSysUiViewModel( onActivation = { isActive = true }, onDeactivation = { isActive = false }, @@ -130,6 +134,7 @@ class SysUiViewModelTest : SysuiTestCase() { val viewModel = FakeViewModel() backgroundScope.launch { view.viewModel( + traceName = "test", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { viewModel }, ) { @@ -151,7 +156,7 @@ class SysUiViewModelTest : SysuiTestCase() { } } -private class FakeViewModel : SysUiViewModel, ExclusiveActivatable() { +private class FakeViewModel : ExclusiveActivatable() { var isActivated = false override suspend fun onActivated(): Nothing { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index 99c5b7cdfdc5..9eccd9fd3cdf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -41,12 +41,11 @@ import android.os.Bundle import android.os.UserHandle import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings import android.service.notification.StatusBarNotification -import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import androidx.media.utils.MediaConstants -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.logging.InstanceId @@ -58,9 +57,15 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.flags.Flags.MEDIA_REMOTE_RESUME +import com.android.systemui.flags.Flags.MEDIA_RESUME_PROGRESS +import com.android.systemui.flags.Flags.MEDIA_RETAIN_RECOMMENDATIONS +import com.android.systemui.flags.Flags.MEDIA_RETAIN_SESSIONS +import com.android.systemui.flags.Flags.MEDIA_SESSION_ACTIONS +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope -import com.android.systemui.media.controls.data.repository.MediaDataRepository -import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.data.repository.mediaDataRepository import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.resume.MediaResumeListener @@ -70,10 +75,10 @@ import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERI import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider -import com.android.systemui.media.controls.util.MediaControllerFactory -import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger -import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.media.controls.util.fakeMediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.statusbar.notificationLockscreenUserManager @@ -81,13 +86,10 @@ import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.time.FakeSystemClock -import com.android.systemui.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.runCurrent import org.junit.After import org.junit.Before import org.junit.Rule @@ -111,6 +113,8 @@ import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private const val KEY = "KEY" private const val KEY_2 = "KEY_2" @@ -134,13 +138,10 @@ private fun <T> anyObject(): T { @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @EnableSceneContainer -class MediaDataProcessorTest : SysuiTestCase() { - val kosmos = testKosmos() - +class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() - @Mock lateinit var mediaControllerFactory: MediaControllerFactory @Mock lateinit var controller: MediaController @Mock lateinit var transportControls: MediaController.TransportControls @Mock lateinit var playbackInfo: MediaController.PlaybackInfo @@ -158,7 +159,6 @@ class MediaDataProcessorTest : SysuiTestCase() { @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest @Mock lateinit var listener: MediaDataManager.Listener @Mock lateinit var pendingIntent: PendingIntent - @Mock lateinit var activityStarter: ActivityStarter @Mock lateinit var smartspaceManager: SmartspaceManager @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor private lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider @@ -166,7 +166,6 @@ class MediaDataProcessorTest : SysuiTestCase() { @Mock private lateinit var mediaRecommendationItem: SmartspaceAction private lateinit var validRecommendationList: List<SmartspaceAction> @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction - @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var logger: MediaUiEventLogger private lateinit var mediaCarouselInteractor: MediaCarouselInteractor private lateinit var mediaDataProcessor: MediaDataProcessor @@ -179,11 +178,30 @@ class MediaDataProcessorTest : SysuiTestCase() { @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig> @Mock private lateinit var ugm: IUriGrantsManager @Mock private lateinit var imageSource: ImageDecoder.Source - private lateinit var mediaDataRepository: MediaDataRepository - private lateinit var testScope: TestScope - private lateinit var testDispatcher: TestDispatcher - private lateinit var testableLooper: TestableLooper - private lateinit var fakeHandler: FakeHandler + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.progressionOf( + Flags.FLAG_MEDIA_LOAD_METADATA_VIA_MEDIA_DATA_LOADER + ) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + + private val kosmos = testKosmos() + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope + private val fakeFeatureFlags = kosmos.fakeFeatureFlagsClassic + private val activityStarter = kosmos.activityStarter + private val mediaControllerFactory = kosmos.fakeMediaControllerFactory + private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager + private val mediaFilterRepository = kosmos.mediaFilterRepository + private val mediaDataFilter = kosmos.mediaDataFilter private val settings = FakeSettings() private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) @@ -194,9 +212,6 @@ class MediaDataProcessorTest : SysuiTestCase() { Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1 ) - private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager - private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository - private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter private lateinit var staticMockSession: MockitoSession @@ -212,17 +227,12 @@ class MediaDataProcessorTest : SysuiTestCase() { foregroundExecutor = FakeExecutor(clock) backgroundExecutor = FakeExecutor(clock) uiExecutor = FakeExecutor(clock) - testableLooper = TestableLooper.get(this) - fakeHandler = FakeHandler(testableLooper.looper) smartspaceMediaDataProvider = SmartspaceMediaDataProvider() Settings.Secure.putInt( context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1 ) - testDispatcher = UnconfinedTestDispatcher() - testScope = TestScope(testDispatcher) - mediaDataRepository = MediaDataRepository(mediaFlags, dumpManager) mediaDataProcessor = MediaDataProcessor( context = context, @@ -231,7 +241,7 @@ class MediaDataProcessorTest : SysuiTestCase() { backgroundExecutor = backgroundExecutor, uiExecutor = uiExecutor, foregroundExecutor = foregroundExecutor, - handler = fakeHandler, + mainDispatcher = testDispatcher, mediaControllerFactory = mediaControllerFactory, broadcastDispatcher = broadcastDispatcher, dumpManager = dumpManager, @@ -241,13 +251,15 @@ class MediaDataProcessorTest : SysuiTestCase() { useQsMediaPlayer = true, systemClock = clock, secureSettings = settings, - mediaFlags = mediaFlags, + mediaFlags = kosmos.mediaFlags, logger = logger, smartspaceManager = smartspaceManager, keyguardUpdateMonitor = keyguardUpdateMonitor, - mediaDataRepository = mediaDataRepository, + mediaDataRepository = kosmos.mediaDataRepository, + mediaDataLoader = { kosmos.mediaDataLoader }, ) mediaDataProcessor.start() + testScope.runCurrent() mediaCarouselInteractor = MediaCarouselInteractor( applicationScope = testScope.backgroundScope, @@ -259,7 +271,7 @@ class MediaDataProcessorTest : SysuiTestCase() { mediaDataCombineLatest = mediaDataCombineLatest, mediaDataFilter = mediaDataFilter, mediaFilterRepository = mediaFilterRepository, - mediaFlags = mediaFlags + mediaFlags = kosmos.mediaFlags ) mediaCarouselInteractor.start() verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor) @@ -295,7 +307,7 @@ class MediaDataProcessorTest : SysuiTestCase() { putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) } verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) - whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) + mediaControllerFactory.setControllerForToken(session.sessionToken, controller) whenever(controller.transportControls).thenReturn(transportControls) whenever(controller.playbackInfo).thenReturn(playbackInfo) whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -325,10 +337,11 @@ class MediaDataProcessorTest : SysuiTestCase() { whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false) - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) - whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, false) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, false) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, false) + fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, false) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, false) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) } @@ -374,6 +387,7 @@ class MediaDataProcessorTest : SysuiTestCase() { PACKAGE_NAME ) + testScope.runCurrent() backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() verify(listener) @@ -399,7 +413,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testLoadsMetadataOnBackground() { mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.numPending()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 0, background = 1) } @Test @@ -416,8 +430,7 @@ class MediaDataProcessorTest : SysuiTestCase() { mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -434,8 +447,7 @@ class MediaDataProcessorTest : SysuiTestCase() { fun testOnMetaDataLoaded_withoutExplicitIndicator() { mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -462,10 +474,8 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testOnMetaDataLoaded_conservesActiveFlag() { - whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -509,8 +519,7 @@ class MediaDataProcessorTest : SysuiTestCase() { } mediaDataProcessor.onNotificationAdded(KEY, notif) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -596,8 +605,7 @@ class MediaDataProcessorTest : SysuiTestCase() { mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) // Then a media control is created with a placeholder title string - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -627,8 +635,7 @@ class MediaDataProcessorTest : SysuiTestCase() { mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) // Then a media control is created with a placeholder title string - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -669,8 +676,7 @@ class MediaDataProcessorTest : SysuiTestCase() { mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) // Then the media control is added using the notification's title - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -778,8 +784,7 @@ class MediaDataProcessorTest : SysuiTestCase() { // GIVEN that the manager has two notifications with resume actions mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) mediaDataProcessor.onNotificationAdded(KEY_2, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) + testScope.assertRunAllReady(foreground = 2, background = 2) verify(listener) .onMediaDataLoaded( @@ -866,7 +871,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowed() { // With the flag enabled to allow remote media to resume - whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, true) // GIVEN that the manager has a notification with a resume action, but is not local whenever(controller.metadata).thenReturn(metadataBuilder.build()) @@ -897,7 +902,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowed() { // With the flag enabled to allow remote media to resume - whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_REMOTE_RESUME, true) // GIVEN that the manager has a remote cast notification addNotificationAndLoad(remoteCastNotification) @@ -1016,7 +1021,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasPartialProgress() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added with partial progress val progress = 0.5 @@ -1043,7 +1048,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasNotPlayedProgress() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that have not been played val extras = @@ -1068,7 +1073,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasFullProgress() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added with progress info val extras = @@ -1094,7 +1099,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasNoExtras() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that do not have any extras val desc = @@ -1112,7 +1117,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasEmptyTitle() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that have empty title val desc = @@ -1131,8 +1136,7 @@ class MediaDataProcessorTest : SysuiTestCase() { ) // Resumption controls are not added. - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + testScope.assertRunAllReady(foreground = 0, background = 1) verify(listener, never()) .onMediaDataLoaded( eq(PACKAGE_NAME), @@ -1146,7 +1150,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testAddResumptionControls_hasBlankTitle() { - whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RESUME_PROGRESS, true) // WHEN resumption controls are added that have a blank title val desc = @@ -1165,8 +1169,7 @@ class MediaDataProcessorTest : SysuiTestCase() { ) // Resumption controls are not added. - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + testScope.assertRunAllReady(foreground = 0, background = 1) verify(listener, never()) .onMediaDataLoaded( eq(PACKAGE_NAME), @@ -1233,8 +1236,7 @@ class MediaDataProcessorTest : SysuiTestCase() { mediaDataProcessor.onNotificationAdded(KEY, notif) // THEN it still loads - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -1351,7 +1353,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) val instanceId = instanceIdSequence.lastInstanceId @@ -1377,7 +1379,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) val extras = Bundle().apply { putString("package_name", PACKAGE_NAME) @@ -1411,7 +1413,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) val instanceId = instanceIdSequence.lastInstanceId @@ -1443,7 +1445,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testSetRecommendationInactive_notifiesListeners() { - whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_RECOMMENDATIONS, true) smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) val instanceId = instanceIdSequence.lastInstanceId @@ -1476,6 +1478,7 @@ class MediaDataProcessorTest : SysuiTestCase() { fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() { // WHEN media recommendation setting is off settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + testScope.runCurrent() smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) @@ -1493,6 +1496,7 @@ class MediaDataProcessorTest : SysuiTestCase() { // WHEN the media recommendation setting is turned off settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + testScope.runCurrent() // THEN listeners are notified uiExecutor.advanceClockToLast() @@ -1513,8 +1517,7 @@ class MediaDataProcessorTest : SysuiTestCase() { fun testOnMediaDataTimedOut_updatesLastActiveTime() { // GIVEN that the manager has a notification mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) // WHEN the notification times out clock.advanceTime(100) @@ -1622,8 +1625,7 @@ class MediaDataProcessorTest : SysuiTestCase() { // WHEN the notification is loaded mediaDataProcessor.onNotificationAdded(KEY, notif) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) // THEN only the first MAX_COMPACT_ACTIONS are actually set verify(listener) @@ -1658,8 +1660,7 @@ class MediaDataProcessorTest : SysuiTestCase() { // WHEN the notification is loaded mediaDataProcessor.onNotificationAdded(KEY, notif) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included verify(listener) @@ -1678,7 +1679,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testPlaybackActions_noState_usesNotification() { val desc = "Notification Action" - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) whenever(controller.playbackState).thenReturn(null) val notifWithAction = @@ -1693,8 +1694,7 @@ class MediaDataProcessorTest : SysuiTestCase() { } mediaDataProcessor.onNotificationAdded(KEY, notifWithAction) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -1713,7 +1713,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testPlaybackActions_hasPrevNext() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY or PlaybackState.ACTION_SKIP_TO_PREVIOUS or @@ -1757,7 +1757,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testPlaybackActions_noPrevNext_usesCustom() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { @@ -1789,7 +1789,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testPlaybackActions_connecting() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY val stateBuilder = PlaybackState.Builder() @@ -1809,87 +1809,85 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) - fun postWithPlaybackActions_drawablesReused() = - kosmos.testScope.runTest { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) - whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) - whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) - val stateActions = - PlaybackState.ACTION_PAUSE or - PlaybackState.ACTION_SKIP_TO_PREVIOUS or - PlaybackState.ACTION_SKIP_TO_NEXT - val stateBuilder = - PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0, 10f) - .setActions(stateActions) - whenever(controller.playbackState).thenReturn(stateBuilder.build()) - val userEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - - mediaDataProcessor.addInternalListener(mediaDataFilter) - mediaDataFilter.mediaDataProcessor = mediaDataProcessor - addNotificationAndLoad() - - assertThat(userEntries).hasSize(1) - val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! - - addNotificationAndLoad() - - assertThat(userEntries).hasSize(1) - val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! - assertThat(secondSemanticActions.playOrPause?.icon) - .isEqualTo(firstSemanticActions.playOrPause?.icon) - assertThat(secondSemanticActions.playOrPause?.background) - .isEqualTo(firstSemanticActions.playOrPause?.background) - assertThat(secondSemanticActions.nextOrCustom?.icon) - .isEqualTo(firstSemanticActions.nextOrCustom?.icon) - assertThat(secondSemanticActions.prevOrCustom?.icon) - .isEqualTo(firstSemanticActions.prevOrCustom?.icon) - } + fun postWithPlaybackActions_drawablesReused() { + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + val stateActions = + PlaybackState.ACTION_PAUSE or + PlaybackState.ACTION_SKIP_TO_PREVIOUS or + PlaybackState.ACTION_SKIP_TO_NEXT + val stateBuilder = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PLAYING, 0, 10f) + .setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + val userEntries by testScope.collectLastValue(mediaFilterRepository.selectedUserEntries) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + assertThat(secondSemanticActions.playOrPause?.icon) + .isEqualTo(firstSemanticActions.playOrPause?.icon) + assertThat(secondSemanticActions.playOrPause?.background) + .isEqualTo(firstSemanticActions.playOrPause?.background) + assertThat(secondSemanticActions.nextOrCustom?.icon) + .isEqualTo(firstSemanticActions.nextOrCustom?.icon) + assertThat(secondSemanticActions.prevOrCustom?.icon) + .isEqualTo(firstSemanticActions.prevOrCustom?.icon) + } @Test @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE) - fun postWithPlaybackActions_drawablesNotReused() = - kosmos.testScope.runTest { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) - whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) - whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) - val stateActions = - PlaybackState.ACTION_PAUSE or - PlaybackState.ACTION_SKIP_TO_PREVIOUS or - PlaybackState.ACTION_SKIP_TO_NEXT - val stateBuilder = - PlaybackState.Builder() - .setState(PlaybackState.STATE_PLAYING, 0, 10f) - .setActions(stateActions) - whenever(controller.playbackState).thenReturn(stateBuilder.build()) - val userEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - - mediaDataProcessor.addInternalListener(mediaDataFilter) - mediaDataFilter.mediaDataProcessor = mediaDataProcessor - addNotificationAndLoad() - - assertThat(userEntries).hasSize(1) - val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! - - addNotificationAndLoad() - - assertThat(userEntries).hasSize(1) - val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! - - assertThat(secondSemanticActions.playOrPause?.icon) - .isNotEqualTo(firstSemanticActions.playOrPause?.icon) - assertThat(secondSemanticActions.playOrPause?.background) - .isNotEqualTo(firstSemanticActions.playOrPause?.background) - assertThat(secondSemanticActions.nextOrCustom?.icon) - .isNotEqualTo(firstSemanticActions.nextOrCustom?.icon) - assertThat(secondSemanticActions.prevOrCustom?.icon) - .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon) - } + fun postWithPlaybackActions_drawablesNotReused() { + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + val stateActions = + PlaybackState.ACTION_PAUSE or + PlaybackState.ACTION_SKIP_TO_PREVIOUS or + PlaybackState.ACTION_SKIP_TO_NEXT + val stateBuilder = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PLAYING, 0, 10f) + .setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + val userEntries by testScope.collectLastValue(mediaFilterRepository.selectedUserEntries) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + + addNotificationAndLoad() + + assertThat(userEntries).hasSize(1) + val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!! + + assertThat(secondSemanticActions.playOrPause?.icon) + .isNotEqualTo(firstSemanticActions.playOrPause?.icon) + assertThat(secondSemanticActions.playOrPause?.background) + .isNotEqualTo(firstSemanticActions.playOrPause?.background) + assertThat(secondSemanticActions.nextOrCustom?.icon) + .isNotEqualTo(firstSemanticActions.nextOrCustom?.icon) + assertThat(secondSemanticActions.prevOrCustom?.icon) + .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon) + } @Test fun testPlaybackActions_reservedSpace() { val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY val stateBuilder = PlaybackState.Builder().setActions(stateActions) customDesc.forEach { @@ -1927,7 +1925,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testPlaybackActions_playPause_hasButton() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val stateActions = PlaybackState.ACTION_PLAY_PAUSE val stateBuilder = PlaybackState.Builder().setActions(stateActions) whenever(controller.playbackState).thenReturn(stateBuilder.build()) @@ -1964,8 +1962,7 @@ class MediaDataProcessorTest : SysuiTestCase() { // update to remote cast mediaDataProcessor.onNotificationAdded(KEY, remoteCastNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(logger) .logPlaybackLocationChange( anyInt(), @@ -2027,7 +2024,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build() whenever(controller.playbackState).thenReturn(state) @@ -2071,6 +2068,7 @@ class MediaDataProcessorTest : SysuiTestCase() { pendingIntent, PACKAGE_NAME ) + testScope.runCurrent() backgroundExecutor.runAllReady() foregroundExecutor.runAllReady() @@ -2149,7 +2147,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_notifPlayer_notifRemoved_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control based on notification is added, times out, and then removed addNotificationAndLoad() @@ -2179,7 +2177,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control based on notification is added and times out addNotificationAndLoad() @@ -2197,7 +2195,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control based on notification is added and then removed, without timing out addNotificationAndLoad() @@ -2214,7 +2212,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_canResume_removeWhileActive_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) // When a media control that supports resumption is added addNotificationAndLoad() @@ -2246,8 +2244,8 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_notifRemoved_doesNotChange() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control with PlaybackState actions is added, times out, @@ -2266,8 +2264,8 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_sessionDestroyed_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control with PlaybackState actions is added, times out, @@ -2300,8 +2298,8 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions is added, and then the session is destroyed @@ -2320,8 +2318,8 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResume() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions and that does allow resumption is added, @@ -2354,7 +2352,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testSessionPlayer_sessionDestroyed_noResume_fullyRemoved() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control with PlaybackState actions is added, times out, @@ -2381,7 +2379,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testSessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions is added, and then the session is destroyed @@ -2400,7 +2398,7 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testSessionPlayer_canResume_destroyedWhileActive_setToResume() { - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) addPlaybackStateAction() // When a media control using session actions and that does allow resumption is added, @@ -2433,8 +2431,8 @@ class MediaDataProcessorTest : SysuiTestCase() { @Test fun testSessionDestroyed_noNotificationKey_stillRemoved() { - whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) - whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + fakeFeatureFlags.set(MEDIA_RETAIN_SESSIONS, true) + fakeFeatureFlags.set(MEDIA_SESSION_ACTIONS, true) // When a notiifcation is added and then removed before it is fully processed mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) @@ -2505,6 +2503,23 @@ class MediaDataProcessorTest : SysuiTestCase() { assertThat(mediaDataCaptor.value.artwork).isNull() } + private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) { + runCurrent() + if (Flags.mediaLoadMetadataViaMediaDataLoader()) { + // It doesn't make much sense to count tasks when we use coroutines in loader + // so this check is skipped in that scenario. + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + } else { + if (background > 0) { + assertThat(backgroundExecutor.runAllReady()).isEqualTo(background) + } + if (foreground > 0) { + assertThat(foregroundExecutor.runAllReady()).isEqualTo(foreground) + } + } + } + /** Helper function to add a basic media notification and capture the resulting MediaData */ private fun addNotificationAndLoad() { addNotificationAndLoad(mediaNotification) @@ -2513,8 +2528,7 @@ class MediaDataProcessorTest : SysuiTestCase() { /** Helper function to add the given notification and capture the resulting MediaData */ private fun addNotificationAndLoad(sbn: StatusBarNotification) { mediaDataProcessor.onNotificationAdded(KEY, sbn) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( eq(KEY), @@ -2548,8 +2562,7 @@ class MediaDataProcessorTest : SysuiTestCase() { pendingIntent, packageName ) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + testScope.assertRunAllReady(foreground = 1, background = 1) verify(listener) .onMediaDataLoaded( diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt index 6a66c4087615..0c8d88065a73 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt @@ -54,6 +54,7 @@ import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.testKosmos import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -125,6 +126,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { private lateinit var mediaData: MediaData @JvmField @Rule val mockito = MockitoJUnit.rule() + private val kosmos = testKosmos() + @Before fun setUp() { fakeFgExecutor = FakeExecutor(FakeSystemClock()) @@ -141,6 +144,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { { localBluetoothManager }, fakeFgExecutor, fakeBgExecutor, + kosmos.mediaDeviceLogger, ) manager.addListener(listener) diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt index a82e5c4496b7..263b0017221a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt @@ -14,7 +14,6 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.concurrent.Executor import kotlinx.coroutines.test.StandardTestDispatcher @@ -30,6 +29,7 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -73,8 +73,8 @@ class UserTrackerImplReceiveTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - whenever(context.user).thenReturn(UserHandle.SYSTEM) - whenever(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context) + `when`(context.user).thenReturn(UserHandle.SYSTEM) + `when`(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context) } @Test @@ -94,7 +94,7 @@ class UserTrackerImplReceiveTest : SysuiTestCase() { tracker.addCallback(callback, executor) val profileID = tracker.userId + 10 - whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> val id = invocation.getArgument<Int>(0) val info = UserInfo(id, "", UserInfo.FLAG_FULL) val infoProfile = diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index 2e2ac3eb7183..774aa517672e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -33,11 +33,9 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.capture -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import com.google.common.truth.TruthJUnit.assume -import java.util.concurrent.Executor import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -56,7 +54,10 @@ import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import java.util.concurrent.Executor + @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -70,19 +71,27 @@ class UserTrackerImplTest : SysuiTestCase() { fun isBackgroundUserTrackerEnabled(): Iterable<Boolean> = listOf(true, false) } - @Mock private lateinit var context: Context + @Mock + private lateinit var context: Context - @Mock private lateinit var userManager: UserManager + @Mock + private lateinit var userManager: UserManager - @Mock private lateinit var iActivityManager: IActivityManager + @Mock + private lateinit var iActivityManager: IActivityManager - @Mock private lateinit var userSwitchingReply: IRemoteCallback + @Mock + private lateinit var userSwitchingReply: IRemoteCallback - @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager + @Mock(stubOnly = true) + private lateinit var dumpManager: DumpManager - @Mock(stubOnly = true) private lateinit var handler: Handler + @Mock(stubOnly = true) + private lateinit var handler: Handler - @Parameterized.Parameter @JvmField var isBackgroundUserTrackerEnabled: Boolean = false + @Parameterized.Parameter + @JvmField + var isBackgroundUserTrackerEnabled: Boolean = false private val testScope = TestScope() private val testDispatcher = StandardTestDispatcher(testScope.testScheduler) @@ -95,380 +104,366 @@ class UserTrackerImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - whenever(context.userId).thenReturn(UserHandle.USER_SYSTEM) - whenever(context.user).thenReturn(UserHandle.SYSTEM) - whenever(context.createContextAsUser(any(), anyInt())).thenAnswer { invocation -> + `when`(context.userId).thenReturn(UserHandle.USER_SYSTEM) + `when`(context.user).thenReturn(UserHandle.SYSTEM) + `when`(context.createContextAsUser(any(), anyInt())).thenAnswer { invocation -> val user = invocation.getArgument<UserHandle>(0) - whenever(context.user).thenReturn(user) - whenever(context.userId).thenReturn(user.identifier) + `when`(context.user).thenReturn(user) + `when`(context.userId).thenReturn(user.identifier) context } - whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> val info = UserInfo(invocation.getArgument<Int>(0), "", UserInfo.FLAG_FULL) listOf(info) } featureFlags.set(Flags.USER_TRACKER_BACKGROUND_CALLBACKS, isBackgroundUserTrackerEnabled) tracker = - UserTrackerImpl( - context, - { featureFlags }, - userManager, - iActivityManager, - dumpManager, - testScope.backgroundScope, - testDispatcher, - handler, - ) + UserTrackerImpl( + context, + { featureFlags }, + userManager, + iActivityManager, + dumpManager, + testScope.backgroundScope, + testDispatcher, + handler, + ) } - @Test fun testNotInitialized() = testScope.runTest { assertThat(tracker.initialized).isFalse() } + @Test + fun testNotInitialized() = testScope.runTest { + assertThat(tracker.initialized).isFalse() + } @Test(expected = IllegalStateException::class) - fun testGetUserIdBeforeInitThrowsException() = testScope.runTest { tracker.userId } + fun testGetUserIdBeforeInitThrowsException() = testScope.runTest { + tracker.userId + } @Test(expected = IllegalStateException::class) - fun testGetUserHandleBeforeInitThrowsException() = testScope.runTest { tracker.userHandle } + fun testGetUserHandleBeforeInitThrowsException() = testScope.runTest { + tracker.userHandle + } @Test(expected = IllegalStateException::class) - fun testGetUserContextBeforeInitThrowsException() = testScope.runTest { tracker.userContext } + fun testGetUserContextBeforeInitThrowsException() = testScope.runTest { + tracker.userContext + } @Test(expected = IllegalStateException::class) - fun testGetUserContentResolverBeforeInitThrowsException() = - testScope.runTest { tracker.userContentResolver } + fun testGetUserContentResolverBeforeInitThrowsException() = testScope.runTest { + tracker.userContentResolver + } @Test(expected = IllegalStateException::class) - fun testGetUserProfilesBeforeInitThrowsException() = testScope.runTest { tracker.userProfiles } + fun testGetUserProfilesBeforeInitThrowsException() = testScope.runTest { + tracker.userProfiles + } @Test - fun testInitialize() = - testScope.runTest { - tracker.initialize(0) + fun testInitialize() = testScope.runTest { + tracker.initialize(0) - assertThat(tracker.initialized).isTrue() - } + assertThat(tracker.initialized).isTrue() + } @Test - fun testReceiverRegisteredOnInitialize() = - testScope.runTest { - tracker.initialize(0) + fun testReceiverRegisteredOnInitialize() = testScope.runTest { + tracker.initialize(0) - val captor = ArgumentCaptor.forClass(IntentFilter::class.java) + val captor = ArgumentCaptor.forClass(IntentFilter::class.java) - verify(context) + verify(context) .registerReceiverForAllUsers(eq(tracker), capture(captor), isNull(), eq(handler)) - with(captor.value) { - assertThat(countActions()).isEqualTo(11) - assertThat(hasAction(Intent.ACTION_LOCALE_CHANGED)).isTrue() - assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue() - assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_ADDED)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_REMOVED)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_AVAILABLE)).isTrue() - assertThat(hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)).isTrue() - } + with(captor.value) { + assertThat(countActions()).isEqualTo(11) + assertThat(hasAction(Intent.ACTION_LOCALE_CHANGED)).isTrue() + assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_ADDED)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_REMOVED)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_AVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_PROFILE_UNAVAILABLE)).isTrue() } + } @Test - fun testInitialValuesSet() = - testScope.runTest { - val testID = 4 - tracker.initialize(testID) + fun testInitialValuesSet() = testScope.runTest { + val testID = 4 + tracker.initialize(testID) - verify(userManager).getProfiles(testID) + verify(userManager).getProfiles(testID) - assertThat(tracker.userId).isEqualTo(testID) - assertThat(tracker.userHandle).isEqualTo(UserHandle.of(testID)) - assertThat(tracker.userContext.userId).isEqualTo(testID) - assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(testID)) - assertThat(tracker.userProfiles).hasSize(1) + assertThat(tracker.userId).isEqualTo(testID) + assertThat(tracker.userHandle).isEqualTo(UserHandle.of(testID)) + assertThat(tracker.userContext.userId).isEqualTo(testID) + assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(testID)) + assertThat(tracker.userProfiles).hasSize(1) - val info = tracker.userProfiles[0] - assertThat(info.id).isEqualTo(testID) - } + val info = tracker.userProfiles[0] + assertThat(info.id).isEqualTo(testID) + } @Test - fun testUserSwitch() = - testScope.runTest { - tracker.initialize(0) - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) - captor.value.onUserSwitching(newID, userSwitchingReply) - runCurrent() - verify(userSwitchingReply).sendResult(any()) - - verify(userManager).getProfiles(newID) - - assertThat(tracker.userId).isEqualTo(newID) - assertThat(tracker.userHandle).isEqualTo(UserHandle.of(newID)) - assertThat(tracker.userContext.userId).isEqualTo(newID) - assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(newID)) - assertThat(tracker.userProfiles).hasSize(1) - - val info = tracker.userProfiles[0] - assertThat(info.id).isEqualTo(newID) - } + fun testUserSwitch() = testScope.runTest { + tracker.initialize(0) + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) + captor.value.onUserSwitching(newID, userSwitchingReply) + runCurrent() + verify(userSwitchingReply).sendResult(any()) + + verify(userManager).getProfiles(newID) + + assertThat(tracker.userId).isEqualTo(newID) + assertThat(tracker.userHandle).isEqualTo(UserHandle.of(newID)) + assertThat(tracker.userContext.userId).isEqualTo(newID) + assertThat(tracker.userContext.user).isEqualTo(UserHandle.of(newID)) + assertThat(tracker.userProfiles).hasSize(1) + + val info = tracker.userProfiles[0] + assertThat(info.id).isEqualTo(newID) + } @Test - fun testManagedProfileAvailable() = - testScope.runTest { - tracker.initialize(0) - val profileID = tracker.userId + 10 - - whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = - UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) - } - - val intent = - Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent) - - assertThat(tracker.userProfiles.map { it.id }) - .containsExactly(tracker.userId, profileID) + fun testManagedProfileAvailable() = testScope.runTest { + tracker.initialize(0) + val profileID = tracker.userId + 10 + + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) } + val intent = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent) + + assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID) + } + @Test - fun testManagedProfileUnavailable() = - testScope.runTest { - tracker.initialize(0) - val profileID = tracker.userId + 10 - - whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = - UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE or UserInfo.FLAG_QUIET_MODE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) - } - - val intent = - Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent) - - assertThat(tracker.userProfiles.map { it.id }) - .containsExactly(tracker.userId, profileID) + fun testManagedProfileUnavailable() = testScope.runTest { + tracker.initialize(0) + val profileID = tracker.userId + 10 + + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE or UserInfo.FLAG_QUIET_MODE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) } + val intent = Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent) + + assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID) + } + @Test - fun testManagedProfileStartedAndRemoved() = - testScope.runTest { - tracker.initialize(0) - val profileID = tracker.userId + 10 - - whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = - UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) - } - - // Managed profile started - val intent = - Intent(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent) - - assertThat(tracker.userProfiles.map { it.id }) - .containsExactly(tracker.userId, profileID) - - whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - listOf(UserInfo(invocation.getArgument(0), "", UserInfo.FLAG_FULL)) - } - - val intent2 = - Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - tracker.onReceive(context, intent2) - - assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId) + fun testManagedProfileStartedAndRemoved() = testScope.runTest { + tracker.initialize(0) + val profileID = tracker.userId + 10 + + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) } - @Test - fun testCallbackNotCalledOnAdd() = - testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() + // Managed profile started + val intent = Intent(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent) - tracker.addCallback(callback, executor) + assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId, profileID) - assertThat(callback.calledOnProfilesChanged).isEqualTo(0) - assertThat(callback.calledOnUserChanged).isEqualTo(0) + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + listOf(UserInfo(invocation.getArgument(0), "", UserInfo.FLAG_FULL)) } + val intent2 = Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + tracker.onReceive(context, intent2) + + assertThat(tracker.userProfiles.map { it.id }).containsExactly(tracker.userId) + } + @Test - fun testCallbackCalledOnUserChanging() = - testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) - captor.value.onUserSwitching(newID, userSwitchingReply) - runCurrent() - - verify(userSwitchingReply).sendResult(any()) - assertThat(callback.calledOnUserChanging).isEqualTo(1) - assertThat(callback.lastUser).isEqualTo(newID) - assertThat(callback.lastUserContext?.userId).isEqualTo(newID) - } + fun testCallbackNotCalledOnAdd() = testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() + + tracker.addCallback(callback, executor) + + assertThat(callback.calledOnProfilesChanged).isEqualTo(0) + assertThat(callback.calledOnUserChanged).isEqualTo(0) + } @Test - fun testAsyncCallbackWaitsUserToChange() = - testScope.runTest { - // Skip this test for CountDownLatch variation. The problem is that there would be a - // deadlock if the callbacks processing runs on the same thread as the callback (which - // is blocked by the latch). Before the change it works because the callbacks are - // processed on a binder thread which is always distinct. - // This is the issue that this feature addresses. - assume().that(isBackgroundUserTrackerEnabled).isTrue() - - tracker.initialize(0) - val callback = TestCallback() - val callbackExecutor = FakeExecutor(FakeSystemClock()) - tracker.addCallback(callback, callbackExecutor) - - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onUserSwitching(newID, userSwitchingReply) - - assertThat(callback.calledOnUserChanging).isEqualTo(0) - verify(userSwitchingReply, never()).sendResult(any()) - - FakeExecutor.exhaustExecutors(callbackExecutor) - runCurrent() - FakeExecutor.exhaustExecutors(callbackExecutor) - runCurrent() - - assertThat(callback.calledOnUserChanging).isEqualTo(1) - verify(userSwitchingReply).sendResult(any()) - } + fun testCallbackCalledOnUserChanging() = testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() + tracker.addCallback(callback, executor) + + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) + captor.value.onUserSwitching(newID, userSwitchingReply) + runCurrent() + + verify(userSwitchingReply).sendResult(any()) + assertThat(callback.calledOnUserChanging).isEqualTo(1) + assertThat(callback.lastUser).isEqualTo(newID) + assertThat(callback.lastUserContext?.userId).isEqualTo(newID) + } @Test - fun testCallbackCalledOnUserChanged() = - testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - - val newID = 5 - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) - captor.value.onUserSwitchComplete(newID) - runCurrent() - - assertThat(callback.calledOnUserChanged).isEqualTo(1) - assertThat(callback.lastUser).isEqualTo(newID) - assertThat(callback.lastUserContext?.userId).isEqualTo(newID) - assertThat(callback.calledOnProfilesChanged).isEqualTo(1) - assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(newID) - } + fun testAsyncCallbackWaitsUserToChange() = testScope.runTest { + // Skip this test for CountDownLatch variation. The problem is that there would be a + // deadlock if the callbacks processing runs on the same thread as the callback (which + // is blocked by the latch). Before the change it works because the callbacks are + // processed on a binder thread which is always distinct. + // This is the issue that this feature addresses. + assume().that(isBackgroundUserTrackerEnabled).isTrue() + + tracker.initialize(0) + val callback = TestCallback() + val callbackExecutor = FakeExecutor(FakeSystemClock()) + tracker.addCallback(callback, callbackExecutor) + + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onUserSwitching(newID, userSwitchingReply) + + assertThat(callback.calledOnUserChanging).isEqualTo(0) + verify(userSwitchingReply, never()).sendResult(any()) + + FakeExecutor.exhaustExecutors(callbackExecutor) + runCurrent() + FakeExecutor.exhaustExecutors(callbackExecutor) + runCurrent() + + assertThat(callback.calledOnUserChanging).isEqualTo(1) + verify(userSwitchingReply).sendResult(any()) + } @Test - fun testCallbackCalledOnUserInfoChanged() = - testScope.runTest { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - val profileID = tracker.userId + 10 - - whenever(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = - UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) - } - - val intent = - Intent(Intent.ACTION_USER_INFO_CHANGED) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - - tracker.onReceive(context, intent) - - assertThat(callback.calledOnUserChanged).isEqualTo(0) - assertThat(callback.calledOnProfilesChanged).isEqualTo(1) - assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID) - } + fun testCallbackCalledOnUserChanged() = testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() + tracker.addCallback(callback, executor) + + val newID = 5 + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID) + captor.value.onUserSwitchComplete(newID) + runCurrent() + + assertThat(callback.calledOnUserChanged).isEqualTo(1) + assertThat(callback.lastUser).isEqualTo(newID) + assertThat(callback.lastUserContext?.userId).isEqualTo(newID) + assertThat(callback.calledOnProfilesChanged).isEqualTo(1) + assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(newID) + } @Test - fun testCallbackRemoved() = - testScope.runTest { - tracker.initialize(0) - val newID = 5 - val profileID = newID + 10 - - val callback = TestCallback() - tracker.addCallback(callback, executor) - tracker.removeCallback(callback) - - val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) - verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onUserSwitching(newID, userSwitchingReply) - runCurrent() - verify(userSwitchingReply).sendResult(any()) - captor.value.onUserSwitchComplete(newID) - - val intentProfiles = - Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - - tracker.onReceive(context, intentProfiles) - - assertThat(callback.calledOnUserChanging).isEqualTo(0) - assertThat(callback.calledOnUserChanged).isEqualTo(0) - assertThat(callback.calledOnProfilesChanged).isEqualTo(0) + fun testCallbackCalledOnUserInfoChanged() = testScope.runTest { + tracker.initialize(0) + val callback = TestCallback() + tracker.addCallback(callback, executor) + val profileID = tracker.userId + 10 + + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) } + val intent = Intent(Intent.ACTION_USER_INFO_CHANGED) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + + tracker.onReceive(context, intent) + + assertThat(callback.calledOnUserChanged).isEqualTo(0) + assertThat(callback.calledOnProfilesChanged).isEqualTo(1) + assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID) + } + + @Test + fun testCallbackRemoved() = testScope.runTest { + tracker.initialize(0) + val newID = 5 + val profileID = newID + 10 + + val callback = TestCallback() + tracker.addCallback(callback, executor) + tracker.removeCallback(callback) + + val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) + verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onUserSwitching(newID, userSwitchingReply) + runCurrent() + verify(userSwitchingReply).sendResult(any()) + captor.value.onUserSwitchComplete(newID) + + val intentProfiles = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) + + tracker.onReceive(context, intentProfiles) + + assertThat(callback.calledOnUserChanging).isEqualTo(0) + assertThat(callback.calledOnUserChanged).isEqualTo(0) + assertThat(callback.calledOnProfilesChanged).isEqualTo(0) + } + private class TestCallback : UserTracker.Callback { var calledOnUserChanging = 0 var calledOnUserChanged = 0 diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index ce79fbde77a3..7bc6d4ae2816 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -132,10 +132,10 @@ class CallChipViewModelTest : SysuiTestCase() { ) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.Basic::class.java) + .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) assertThat(icon.contentDescription).isNotNull() @@ -170,10 +170,10 @@ class CallChipViewModelTest : SysuiTestCase() { ) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.Basic::class.java) + .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) assertThat(icon.contentDescription).isNotNull() @@ -206,10 +206,10 @@ class CallChipViewModelTest : SysuiTestCase() { repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = null)) assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.Basic::class.java) + .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) assertThat(icon.contentDescription).isNotNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt index a8d2c5b4cdd7..77992dbaecc2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt @@ -127,7 +127,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected) assertThat((icon.contentDescription as ContentDescription.Resource).res) @@ -146,7 +146,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected) assertThat((icon.contentDescription as ContentDescription.Resource).res) @@ -184,7 +184,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected) // This content description is just generic "Casting", not "Casting screen" @@ -214,7 +214,7 @@ class CastToOtherDeviceChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_cast_connected) // MediaProjection == screen casting, so this content description reflects that we're diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt new file mode 100644 index 000000000000..8576893216d0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt @@ -0,0 +1,129 @@ +/* + * 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.statusbar.chips.ron.demo.ui.viewmodel + +import android.content.packageManager +import android.graphics.drawable.BitmapDrawable +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.test.Test +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +@SmallTest +class DemoRonChipViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val commandRegistry = kosmos.commandRegistry + private val pw = PrintWriter(StringWriter()) + + private val underTest = kosmos.demoRonChipViewModel + + @Before + fun setUp() { + underTest.start() + whenever(kosmos.packageManager.getApplicationIcon(any<String>())) + .thenReturn(BitmapDrawable()) + } + + @Test + @DisableFlags(FLAG_STATUS_BAR_RON_CHIPS) + fun chip_flagOff_hidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + addDemoRonChip() + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) + fun chip_noPackage_hidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + commandRegistry.onShellCommand(pw, arrayOf("demo-ron")) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) + fun chip_hasPackage_shown() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "-p", "com.android.systemui")) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) + fun chip_hasText_shownWithText() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + commandRegistry.onShellCommand( + pw, + arrayOf("demo-ron", "-p", "com.android.systemui", "-t", "test") + ) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Text::class.java) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) + fun chip_hasHideArg_hidden() = + testScope.runTest { + val latest by collectLastValue(underTest.chip) + + // First, show a chip + addDemoRonChip() + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + + // Then, hide the chip + commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "--hide")) + + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java) + } + + private fun addDemoRonChip() { + Companion.addDemoRonChip(commandRegistry, pw) + } + + companion object { + fun addDemoRonChip(commandRegistry: CommandRegistry, pw: PrintWriter) { + commandRegistry.onShellCommand(pw, arrayOf("demo-ron", "-p", "com.android.systemui")) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index 804eb5cf597c..16101bfe387c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -150,7 +150,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_screenrecord) assertThat(icon.contentDescription).isNotNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt index a2ef59916ff6..791a21d0fb63 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt @@ -135,7 +135,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all) assertThat(icon.contentDescription).isNotNull() @@ -152,7 +152,7 @@ class ShareToAppChipViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all) assertThat(icon.contentDescription).isNotNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt index a724cfaa4798..4977c548fb92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt @@ -154,5 +154,7 @@ class ChipTransitionHelperTest : SysuiTestCase() { } private fun createIcon(@DrawableRes drawable: Int) = - OngoingActivityChipModel.ChipIcon.Basic(Icon.Resource(drawable, contentDescription = null)) + OngoingActivityChipModel.ChipIcon.SingleColorIcon( + Icon.Resource(drawable, contentDescription = null) + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index 556ec6a307ab..bd5df07b7ece 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -19,9 +19,12 @@ package com.android.systemui.statusbar.chips.ui.viewmodel import android.content.DialogInterface import android.content.packageManager import android.content.pm.PackageManager +import android.graphics.drawable.BitmapDrawable +import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_STATUS_BAR_RON_CHIPS import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -36,8 +39,11 @@ import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection +import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.DemoRonChipViewModelTest.Companion.addDemoRonChip +import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.commandline.commandRegistry import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository @@ -45,6 +51,8 @@ import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCall import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat +import java.io.PrintWriter +import java.io.StringWriter import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -68,11 +76,14 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope private val systemClock = kosmos.fakeSystemClock + private val commandRegistry = kosmos.commandRegistry private val screenRecordState = kosmos.screenRecordRepository.screenRecordState private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState private val callRepo = kosmos.ongoingCallRepository + private val pw = PrintWriter(StringWriter()) + private val mockSystemUIDialog = mock<SystemUIDialog>() private val chipBackgroundView = mock<ChipBackgroundContainer>() private val chipView = @@ -90,6 +101,9 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { @Before fun setUp() { setUpPackageManagerForMediaProjection(kosmos) + kosmos.demoRonChipViewModel.start() + whenever(kosmos.packageManager.getApplicationIcon(any<String>())) + .thenReturn(BitmapDrawable()) } @Test @@ -169,15 +183,24 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) fun chip_higherPriorityChipAdded_lowerPriorityChipReplaced() = testScope.runTest { - // Start with just the lower priority call chip - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + // Start with just the lowest priority chip shown + addDemoRonChip(commandRegistry, pw) + // And everything else hidden + callRepo.setOngoingCallState(OngoingCallModel.NoCall) mediaProjectionState.value = MediaProjectionState.NotProjecting screenRecordState.value = ScreenRecordModel.DoingNothing val latest by collectLastValue(underTest.chip) + assertIsDemoRonChip(latest) + + // WHEN the higher priority call chip is added + callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + + // THEN the higher priority call chip is used assertIsCallChip(latest) // WHEN the higher priority media projection chip is added @@ -199,14 +222,15 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_STATUS_BAR_RON_CHIPS) fun chip_highestPriorityChipRemoved_showsNextPriorityChip() = testScope.runTest { // WHEN all chips are active screenRecordState.value = ScreenRecordModel.Recording mediaProjectionState.value = MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE) - callRepo.setOngoingCallState(inCallModel(startTimeMs = 34)) + addDemoRonChip(commandRegistry, pw) val latest by collectLastValue(underTest.chip) @@ -224,6 +248,12 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { // THEN the lower priority call is used assertIsCallChip(latest) + + // WHEN the higher priority call is removed + callRepo.setOngoingCallState(OngoingCallModel.NoCall) + + // THEN the lower priority demo RON is used + assertIsDemoRonChip(latest) } /** Regression test for b/347726238. */ @@ -338,7 +368,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_screenrecord) } @@ -347,7 +377,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all) } @@ -356,9 +386,15 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() { assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) val icon = (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.Basic) + as OngoingActivityChipModel.ChipIcon.SingleColorIcon) .impl as Icon.Resource assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) } + + fun assertIsDemoRonChip(latest: OngoingActivityChipModel?) { + assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java) + assertThat((latest as OngoingActivityChipModel.Shown).icon) + .isInstanceOf(OngoingActivityChipModel.ChipIcon.FullColorAppIcon::class.java) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index caa177908db0..1e2648b228f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -257,13 +257,13 @@ public class VolumeDialogImplTest extends SysuiTestCase { private State createShellState() { State state = new VolumeDialogController.State(); - for (int i = AudioManager.STREAM_VOICE_CALL; i <= AudioManager.STREAM_ACCESSIBILITY; i++) { + for (int stream : STREAMS.keySet()) { VolumeDialogController.StreamState ss = new VolumeDialogController.StreamState(); - ss.name = STREAMS.get(i); + ss.name = STREAMS.get(stream); ss.level = 1; ss.levelMin = 0; ss.levelMax = 25; - state.states.append(i, ss); + state.states.append(stream, ss); } return state; } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index 616f2b688746..a73c184a1ba8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -102,6 +102,45 @@ class FakeKeyguardTransitionRepository( } /** + * Sends the provided [step] and makes sure that all previous [TransitionState]'s are sent when + * [fillInSteps] is true. e.g. when a step FINISHED is provided, a step with STARTED and RUNNING + * is also sent. + */ + suspend fun sendTransitionSteps( + step: TransitionStep, + testScope: TestScope, + fillInSteps: Boolean = true, + ) { + if (fillInSteps && step.transitionState != TransitionState.STARTED) { + sendTransitionStep( + step = + TransitionStep( + transitionState = TransitionState.STARTED, + from = step.from, + to = step.to, + value = 0f, + ) + ) + testScope.testScheduler.runCurrent() + + if (step.transitionState != TransitionState.RUNNING) { + sendTransitionStep( + step = + TransitionStep( + transitionState = TransitionState.RUNNING, + from = step.from, + to = step.to, + value = 0.6f, + ) + ) + testScope.testScheduler.runCurrent() + } + } + sendTransitionStep(step = step) + testScope.testScheduler.runCurrent() + } + + /** * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. * * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt index b68d6a0510d5..8e8f4b69e401 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt @@ -27,7 +27,6 @@ val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by KeyguardTransitionInteractor( scope = applicationCoroutineScope, repository = keyguardTransitionRepository, - keyguardRepository = keyguardRepository, fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor }, fromPrimaryBouncerTransitionInteractor = { fromPrimaryBouncerTransitionInteractor }, fromAodTransitionInteractor = { fromAodTransitionInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt index 165246284b5f..2eb7ce603597 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/lifecycle/FakeSysUiViewModel.kt @@ -26,17 +26,25 @@ import kotlinx.coroutines.flow.flowOf class FakeSysUiViewModel( private val onActivation: () -> Unit = {}, private val onDeactivation: () -> Unit = {}, - private val upstreamFlow: Flow<Boolean> = flowOf(true), - private val upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(), -) : SysUiViewModel, ExclusiveActivatable() { + upstreamFlow: Flow<Boolean> = flowOf(true), + upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(), +) : ExclusiveActivatable() { var activationCount = 0 var cancellationCount = 0 - private val hydrator = Hydrator() + private val hydrator = Hydrator("test") val stateBackedByFlow: Boolean by - hydrator.hydratedStateOf(initialValue = true, source = upstreamFlow) - val stateBackedByStateFlow: Boolean by hydrator.hydratedStateOf(source = upstreamStateFlow) + hydrator.hydratedStateOf( + traceName = "test", + initialValue = true, + source = upstreamFlow, + ) + val stateBackedByStateFlow: Boolean by + hydrator.hydratedStateOf( + traceName = "test", + source = upstreamStateFlow, + ) override suspend fun onActivated(): Nothing { activationCount++ diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt index 2127a88e5a45..632436a4574a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.media.controls.domain.pipeline import android.app.smartspace.SmartspaceManager import android.content.applicationContext -import android.os.fakeExecutorHandler import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.concurrency.fakeExecutor @@ -45,7 +44,7 @@ val Kosmos.mediaDataProcessor by backgroundExecutor = fakeExecutor, uiExecutor = fakeExecutor, foregroundExecutor = fakeExecutor, - handler = fakeExecutorHandler, + mainDispatcher = testDispatcher, mediaControllerFactory = fakeMediaControllerFactory, broadcastDispatcher = broadcastDispatcher, dumpManager = dumpManager, @@ -60,5 +59,6 @@ val Kosmos.mediaDataProcessor by smartspaceManager = SmartspaceManager(applicationContext), keyguardUpdateMonitor = keyguardUpdateMonitor, mediaDataRepository = mediaDataRepository, + mediaDataLoader = { mediaDataLoader }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceLoggerKosmos.kt new file mode 100644 index 000000000000..76d71dd05edd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.media.controls.domain.pipeline + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +var Kosmos.mediaDeviceLogger by + Kosmos.Fixture { MediaDeviceLogger(logcatLogBuffer("MediaDeviceLoggerKosmos")) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt index c479ce676761..11408d8a4b90 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt @@ -41,5 +41,6 @@ val Kosmos.mediaDeviceManager by }, fgExecutor = fakeExecutor, bgExecutor = fakeExecutor, + logger = mediaDeviceLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt index 53d3c0121b1f..59f2b9412413 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryUtil.kt @@ -19,6 +19,7 @@ package com.android.systemui.scene.data.repository import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope @@ -36,20 +37,26 @@ private val mutableTransitionState = suspend fun Kosmos.setTransition( sceneTransition: ObservableTransitionState, stateTransition: TransitionStep? = null, + fillInStateSteps: Boolean = true, scope: TestScope = testScope, repository: SceneContainerRepository = sceneContainerRepository ) { + var state: TransitionStep? = stateTransition if (SceneContainerFlag.isEnabled) { setSceneTransition(sceneTransition, scope, repository) - } else { - if (stateTransition == null) throw IllegalArgumentException("No transitionStep provided") - fakeKeyguardTransitionRepository.sendTransitionSteps( - from = stateTransition.from, - to = stateTransition.to, - testScope = scope, - throughTransitionState = stateTransition.transitionState - ) + + if (state != null) { + state = getStateWithUndefined(sceneTransition, state) + } } + + if (state == null) return + fakeKeyguardTransitionRepository.sendTransitionSteps( + step = state, + testScope = scope, + fillInSteps = fillInStateSteps, + ) + scope.testScheduler.runCurrent() } fun Kosmos.setSceneTransition( @@ -59,7 +66,7 @@ fun Kosmos.setSceneTransition( ) { repository.setTransitionState(mutableTransitionState) mutableTransitionState.value = transition - scope.runCurrent() + scope.testScheduler.runCurrent() } fun Transition( @@ -87,3 +94,43 @@ fun Transition( fun Idle(currentScene: SceneKey): ObservableTransitionState.Idle { return ObservableTransitionState.Idle(currentScene) } + +private fun getStateWithUndefined( + sceneTransition: ObservableTransitionState, + state: TransitionStep +): TransitionStep { + return when (sceneTransition) { + is ObservableTransitionState.Idle -> { + TransitionStep( + from = state.from, + to = + if (sceneTransition.currentScene != Scenes.Lockscreen) { + KeyguardState.UNDEFINED + } else { + state.to + }, + value = state.value, + transitionState = state.transitionState + ) + } + is ObservableTransitionState.Transition -> { + TransitionStep( + from = + if (sceneTransition.fromScene != Scenes.Lockscreen) { + KeyguardState.UNDEFINED + } else { + state.from + }, + to = + if (sceneTransition.toScene != Scenes.Lockscreen) { + KeyguardState.UNDEFINED + } else { + state.from + }, + value = state.value, + transitionState = state.transitionState + ) + } + else -> state + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.kt new file mode 100644 index 000000000000..c0d65a076ca0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelKosmos.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.systemui.statusbar.chips.ron.demo.ui.viewmodel + +import android.content.packageManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.util.time.fakeSystemClock + +val Kosmos.demoRonChipViewModel: DemoRonChipViewModel by + Kosmos.Fixture { + DemoRonChipViewModel( + commandRegistry = commandRegistry, + packageManager = packageManager, + systemClock = fakeSystemClock, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt index 16e288fcf113..5382c1c4b8d0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.chips.call.ui.viewmodel.callChipViewModel import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.castToOtherDeviceChipViewModel +import com.android.systemui.statusbar.chips.ron.demo.ui.viewmodel.demoRonChipViewModel import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.screenRecordChipViewModel import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.shareToAppChipViewModel import com.android.systemui.statusbar.chips.statusBarChipsLogger @@ -32,6 +33,7 @@ val Kosmos.ongoingActivityChipsViewModel: OngoingActivityChipsViewModel by shareToAppChipViewModel = shareToAppChipViewModel, castToOtherDeviceChipViewModel = castToOtherDeviceChipViewModel, callChipViewModel = callChipViewModel, + demoRonChipViewModel = demoRonChipViewModel, logger = statusBarChipsLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/commandline/CommandRegistryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/commandline/CommandRegistryKosmos.kt new file mode 100644 index 000000000000..14777b4bc492 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/commandline/CommandRegistryKosmos.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.statusbar.commandline + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +val Kosmos.commandRegistry: CommandRegistry by + Kosmos.Fixture { + CommandRegistry( + context = applicationContext, + // Immediately run anything that comes in + mainExecutor = { command -> command.run() }, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt index 63386d016a7a..dd5bbf37c0f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/domain/interactor/AudioSlidersInteractorKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.volume.panel.component.volume.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.volume.data.repository.audioRepository import com.android.systemui.volume.domain.interactor.audioModeInteractor import com.android.systemui.volume.mediaOutputInteractor @@ -27,7 +26,6 @@ val Kosmos.audioSlidersInteractor by AudioSlidersInteractor( applicationCoroutineScope, mediaOutputInteractor, - audioRepository, audioModeInteractor, ) } diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index be4cd761a4ec..9b0c8e554d64 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -160,6 +160,7 @@ java_library { "ravenwood-framework", "services.core.ravenwood", "junit", + "framework-annotations-lib", ], sdk_version: "core_current", visibility: ["//frameworks/base"], diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java index 3a24c0e829a4..e8f59db86901 100644 --- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java @@ -26,6 +26,10 @@ import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; +/** + * Test to ensure @DisabledOnRavenwood works. Note, now the DisabledOnRavenwood annotation + * is handled by the test runner, so it won't really need the class rule. + */ @RunWith(AndroidJUnit4.class) @DisabledOnRavenwood public class RavenwoodClassRuleDeviceOnlyTest { diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java index 0f8be0eeebeb..7ef672e80bee 100644 --- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java @@ -34,7 +34,12 @@ public class RavenwoodImplicitClassRuleDeviceOnlyTest { @BeforeClass public static void beforeClass() { - Assert.assertFalse(RavenwoodRule.isOnRavenwood()); + // This method shouldn't be called -- unless RUN_DISABLED_TESTS is enabled. + + // If we're doing RUN_DISABLED_TESTS, don't throw here, because that'd confuse junit. + if (!RavenwoodRule.private$ravenwood().isRunningDisabledTests()) { + Assert.assertFalse(RavenwoodRule.isOnRavenwood()); + } } @Test @@ -46,7 +51,10 @@ public class RavenwoodImplicitClassRuleDeviceOnlyTest { public static void afterClass() { if (RavenwoodRule.isOnRavenwood()) { Log.e(TAG, "Even @AfterClass shouldn't be executed!"); - System.exit(1); + + if (!RavenwoodRule.private$ravenwood().isRunningDisabledTests()) { + System.exit(1); + } } } } diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java new file mode 100644 index 000000000000..c77841b1b55a --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java @@ -0,0 +1,88 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import static org.junit.Assert.fail; + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing; +import android.platform.test.ravenwood.RavenwoodRule; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test for "RAVENWOOD_RUN_DISABLED_TESTS" with "REALLY_DISABLED" set. + * + * This test is only executed on Ravenwood. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodRunDisabledTestsReallyDisabledTest { + private static final String TAG = "RavenwoodRunDisabledTestsTest"; + + private static final CallTracker sCallTracker = new CallTracker(); + + @RavenwoodTestRunnerInitializing + public static void ravenwoodRunnerInitializing() { + RavenwoodRule.private$ravenwood().overrideRunDisabledTest(true, + "\\#testReallyDisabled$"); + } + + /** + * This test gets to run with RAVENWOOD_RUN_DISABLED_TESTS set. + */ + @Test + @DisabledOnRavenwood + public void testDisabledTestGetsToRun() { + if (!RavenwoodRule.isOnRavenwood()) { + return; + } + sCallTracker.incrementMethodCallCount(); + + fail("This test won't pass on Ravenwood."); + } + + /** + * This will still not be executed due to the "really disabled" pattern. + */ + @Test + @DisabledOnRavenwood + public void testReallyDisabled() { + if (!RavenwoodRule.isOnRavenwood()) { + return; + } + sCallTracker.incrementMethodCallCount(); + + fail("This test won't pass on Ravenwood."); + } + + @AfterClass + public static void afterClass() { + if (!RavenwoodRule.isOnRavenwood()) { + return; + } + Log.i(TAG, "afterClass called"); + + sCallTracker.assertCallsOrDie( + "testDisabledTestGetsToRun", 1, + "testReallyDisabled", 0 + ); + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java new file mode 100644 index 000000000000..ea1a29d57482 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java @@ -0,0 +1,87 @@ +/* + * 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.ravenwoodtest.bivalenttest.ravenizer; + +import static org.junit.Assert.fail; + +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing; +import android.platform.test.ravenwood.RavenwoodRule; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +/** + * Test for "RAVENWOOD_RUN_DISABLED_TESTS". (with no "REALLY_DISABLED" set.) + * + * This test is only executed on Ravenwood. + */ +@RunWith(AndroidJUnit4.class) +public class RavenwoodRunDisabledTestsTest { + private static final String TAG = "RavenwoodRunDisabledTestsTest"; + + @Rule + public ExpectedException mExpectedException = ExpectedException.none(); + + private static final CallTracker sCallTracker = new CallTracker(); + + @RavenwoodTestRunnerInitializing + public static void ravenwoodRunnerInitializing() { + RavenwoodRule.private$ravenwood().overrideRunDisabledTest(true, null); + } + + @Test + @DisabledOnRavenwood + public void testDisabledTestGetsToRun() { + if (!RavenwoodRule.isOnRavenwood()) { + return; + } + sCallTracker.incrementMethodCallCount(); + + fail("This test won't pass on Ravenwood."); + } + + @Test + @DisabledOnRavenwood + public void testDisabledButPass() { + if (!RavenwoodRule.isOnRavenwood()) { + return; + } + sCallTracker.incrementMethodCallCount(); + + // When a @DisabledOnRavenwood actually passed, the runner should make fail(). + mExpectedException.expectMessage("it actually passed under Ravenwood"); + } + + @AfterClass + public static void afterClass() { + if (!RavenwoodRule.isOnRavenwood()) { + return; + } + Log.i(TAG, "afterClass called"); + + sCallTracker.assertCallsOrDie( + "testDisabledTestGetsToRun", 1, + "testDisabledButPass", 1 + ); + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java index 03600ad5511f..1da93eba94f7 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -17,14 +17,16 @@ package android.platform.test.ravenwood; import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP; +import static org.junit.Assert.fail; + import android.os.Bundle; import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order; import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope; +import android.platform.test.ravenwood.RavenwoodTestStats.Result; +import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.ravenwood.common.RavenwoodCommonUtils; - import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runners.model.TestClass; @@ -38,12 +40,24 @@ public class RavenwoodAwareTestRunnerHook { private RavenwoodAwareTestRunnerHook() { } - private static void log(String message) { - RavenwoodCommonUtils.log(TAG, message); + private static RavenwoodTestStats sStats; // lazy initialization. + private static Description sCurrentClassDescription; + + private static RavenwoodTestStats getStats() { + if (sStats == null) { + // We don't want to throw in the static initializer, because tradefed may not report + // it properly, so we initialize it here. + sStats = new RavenwoodTestStats(); + } + return sStats; } + /** + * Called when a runner starts, before the inner runner gets a chance to run. + */ public static void onRunnerInitializing(Runner runner, TestClass testClass) { - log("onRunnerStart: testClass=" + testClass + " runner=" + runner); + // This log call also ensures the framework JNI is loaded. + Log.i(TAG, "onRunnerInitializing: testClass=" + testClass + " runner=" + runner); // TODO: Move the initialization code to a better place. @@ -52,26 +66,97 @@ public class RavenwoodAwareTestRunnerHook { "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"); System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1"); + // This is needed to make AndroidJUnit4ClassRunner happy. InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); } + /** + * Called when a whole test class is skipped. + */ + public static void onClassSkipped(Description description) { + Log.i(TAG, "onClassSkipped: description=" + description); + getStats().onClassSkipped(description); + } + + /** + * Called before a test / class. + * + * Return false if it should be skipped. + */ public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description, Scope scope, Order order) { - log("onBefore: description=" + description + ", " + scope + ", " + order); + Log.i(TAG, "onBefore: description=" + description + ", " + scope + ", " + order); + + if (scope == Scope.Class && order == Order.First) { + // Keep track of the current class. + sCurrentClassDescription = description; + } // Class-level annotations are checked by the runner already, so we only check // method-level annotations here. if (scope == Scope.Instance && order == Order.First) { - if (!RavenwoodRule.shouldEnableOnRavenwood(description)) { + if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood( + description, true)) { + getStats().onTestFinished(sCurrentClassDescription, description, Result.Skipped); return false; } } return true; } - public static void onAfter(RavenwoodAwareTestRunner runner, Description description, + /** + * Called after a test / class. + * + * Return false if the exception should be ignored. + */ + public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description, Scope scope, Order order, Throwable th) { - log("onAfter: description=" + description + ", " + scope + ", " + order + ", " + th); + Log.i(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th); + + if (scope == Scope.Instance && order == Order.First) { + getStats().onTestFinished(sCurrentClassDescription, description, + th == null ? Result.Passed : Result.Failed); + + } else if (scope == Scope.Class && order == Order.Last) { + getStats().onClassFinished(sCurrentClassDescription); + } + + // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error. + if (RavenwoodRule.private$ravenwood().isRunningDisabledTests() + && scope == Scope.Instance && order == Order.First) { + + boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood( + description, false); + if (th == null) { + // Test passed. Is the test method supposed to be enabled? + if (isTestEnabled) { + // Enabled and didn't throw, okay. + return true; + } else { + // Disabled and didn't throw. We should report it. + fail("Test wasn't included under Ravenwood, but it actually " + + "passed under Ravenwood; consider updating annotations"); + return true; // unreachable. + } + } else { + // Test failed. + if (isTestEnabled) { + // Enabled but failed. We should throw the exception. + return true; + } else { + // Disabled and failed. Expected. Don't throw. + return false; + } + } + } + return true; + } + + /** + * Called by {@link RavenwoodAwareTestRunner} to see if it should run a test class or not. + */ + public static boolean shouldRunClassOnRavenwood(Class<?> clazz) { + return RavenwoodEnablementChecker.shouldRunClassOnRavenwood(clazz, true); } } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java new file mode 100644 index 000000000000..77275c445dd9 --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.annotations.EnabledOnRavenwood; +import android.platform.test.annotations.IgnoreUnderRavenwood; + +import org.junit.runner.Description; + +/** + * Calculates which tests need to be executed on Ravenwood. + */ +public class RavenwoodEnablementChecker { + private static final String TAG = "RavenwoodDisablementChecker"; + + private RavenwoodEnablementChecker() { + } + + /** + * Determine if the given {@link Description} should be enabled when running on the + * Ravenwood test environment. + * + * A more specific method-level annotation always takes precedence over any class-level + * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over + * an {@link DisabledOnRavenwood} annotation. + */ + public static boolean shouldEnableOnRavenwood(Description description, + boolean takeIntoAccountRunDisabledTestsFlag) { + // First, consult any method-level annotations + if (description.isTest()) { + Boolean result = null; + + // Stopgap for http://g/ravenwood/EPAD-N5ntxM + if (description.getMethodName().endsWith("$noRavenwood")) { + result = false; + } else if (description.getAnnotation(EnabledOnRavenwood.class) != null) { + result = true; + } else if (description.getAnnotation(DisabledOnRavenwood.class) != null) { + result = false; + } else if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) { + result = false; + } + if (result != null) { + if (takeIntoAccountRunDisabledTestsFlag + && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) { + result = !shouldStillIgnoreInProbeIgnoreMode( + description.getTestClass(), description.getMethodName()); + } + } + if (result != null) { + return result; + } + } + + // Otherwise, consult any class-level annotations + return shouldRunClassOnRavenwood(description.getTestClass(), + takeIntoAccountRunDisabledTestsFlag); + } + + public static boolean shouldRunClassOnRavenwood(@NonNull Class<?> testClass, + boolean takeIntoAccountRunDisabledTestsFlag) { + boolean result = true; + if (testClass.getAnnotation(EnabledOnRavenwood.class) != null) { + result = true; + } else if (testClass.getAnnotation(DisabledOnRavenwood.class) != null) { + result = false; + } else if (testClass.getAnnotation(IgnoreUnderRavenwood.class) != null) { + result = false; + } + if (!result) { + if (takeIntoAccountRunDisabledTestsFlag + && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) { + result = !shouldStillIgnoreInProbeIgnoreMode(testClass, null); + } + } + return result; + } + + /** + * Check if a test should _still_ disabled even if {@code RUN_DISABLED_TESTS} + * is true, using {@code REALLY_DISABLED_PATTERN}. + * + * This only works on tests, not on classes. + */ + static boolean shouldStillIgnoreInProbeIgnoreMode( + @NonNull Class<?> testClass, @Nullable String methodName) { + if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().pattern().isEmpty()) { + return false; + } + + final var fullname = testClass.getName() + (methodName != null ? "#" + methodName : ""); + + System.out.println("XXX=" + fullname); + + if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().matcher(fullname).find()) { + System.out.println("Still ignoring " + fullname); + return true; + } + return false; + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 7b4c17390942..a2088fd0b77f 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -40,7 +40,6 @@ import com.android.internal.os.RuntimeInit; import com.android.server.LocalServices; import org.junit.runner.Description; -import org.junit.runners.model.Statement; import java.io.File; import java.io.IOException; @@ -226,11 +225,6 @@ public class RavenwoodRuleImpl { } } - public static void validate(Statement base, Description description, - boolean enableOptionalValidation) { - // Nothing to check, for now. - } - /** * Set the current configuration to the actual SystemProperties. */ diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java new file mode 100644 index 000000000000..631f68ff1dec --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import android.util.Log; + +import org.junit.runner.Description; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +/** + * Creats a "stats" CSV file containing the test results. + * + * The output file is created as `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_[TIMESTAMP].csv`. + * A symlink to the latest result will be created as + * `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_latest.csv`. + */ +public class RavenwoodTestStats { + private static final String TAG = "RavenwoodTestStats"; + private static final String HEADER = "Module,Class,ClassDesc,Passed,Failed,Skipped"; + + public enum Result { + Passed, + Failed, + Skipped, + } + + private final File mOutputFile; + private final PrintWriter mOutputWriter; + private final String mTestModuleName; + + public final Map<Description, Map<Description, Result>> mStats = new HashMap<>(); + + /** Ctor */ + public RavenwoodTestStats() { + mTestModuleName = guessTestModuleName(); + + var basename = "Ravenwood-stats_" + mTestModuleName + "_"; + + // Get the current time + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + + var tmpdir = System.getProperty("java.io.tmpdir"); + mOutputFile = new File(tmpdir, basename + now.format(fmt) + ".csv"); + + try { + mOutputWriter = new PrintWriter(mOutputFile); + } catch (IOException e) { + throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e); + } + + // Crete the "latest" symlink. + Path symlink = Paths.get(tmpdir, basename + "latest.csv"); + try { + if (Files.exists(symlink)) { + Files.delete(symlink); + } + Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName())); + + } catch (IOException e) { + throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e); + } + + Log.i(TAG, "Test result stats file: " + mOutputFile); + + // Print the header. + mOutputWriter.println(HEADER); + mOutputWriter.flush(); + } + + private String guessTestModuleName() { + // Assume the current directory name is the test module name. + File cwd; + try { + cwd = new File(".").getCanonicalFile(); + } catch (IOException e) { + throw new RuntimeException("Failed to get the current directory", e); + } + return cwd.getName(); + } + + private void addResult(Description classDescription, Description methodDescription, + Result result) { + mStats.compute(classDescription, (classDesc, value) -> { + if (value == null) { + value = new HashMap<>(); + } + value.put(methodDescription, result); + return value; + }); + } + + public void onClassSkipped(Description classDescription) { + addResult(classDescription, Description.EMPTY, Result.Skipped); + onClassFinished(classDescription); + } + + public void onTestFinished(Description classDescription, Description testDescription, + Result result) { + addResult(classDescription, testDescription, result); + } + + public void onClassFinished(Description classDescription) { + int passed = 0; + int skipped = 0; + int failed = 0; + for (var e : mStats.get(classDescription).values()) { + switch (e) { + case Passed: passed++; break; + case Skipped: skipped++; break; + case Failed: failed++; break; + } + } + + var testClass = extractTestClass(classDescription); + + mOutputWriter.printf("%s,%s,%s,%d,%d,%d\n", + mTestModuleName, (testClass == null ? "?" : testClass.getCanonicalName()), + classDescription, passed, failed, skipped); + mOutputWriter.flush(); + } + + /** + * Try to extract the class from a description, which is needed because + * ParameterizedAndroidJunit4's description doesn't contain a class. + */ + private Class<?> extractTestClass(Description desc) { + if (desc.getTestClass() != null) { + return desc.getTestClass(); + } + // Look into the children. + for (var child : desc.getChildren()) { + var fromChild = extractTestClass(child); + if (fromChild != null) { + return fromChild; + } + } + return null; + } +} diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index a4fa41af26e5..2b55ac52ab75 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -15,8 +15,6 @@ */ package android.platform.test.ravenwood; -import static android.platform.test.ravenwood.RavenwoodRule.shouldRunCassOnRavenwood; - import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod; import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood; @@ -164,7 +162,8 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde * If the class has @DisabledOnRavenwood, then we'll delegate to ClassSkippingTestRunner, * which simply skips it. */ - if (isOnRavenwood() && !shouldRunCassOnRavenwood(mTestClsas.getJavaClass())) { + if (isOnRavenwood() && !RavenwoodAwareTestRunnerHook.shouldRunClassOnRavenwood( + mTestClsas.getJavaClass())) { mRealRunner = new ClassSkippingTestRunner(mTestClsas); return; } @@ -238,6 +237,7 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde public void run(RunNotifier notifier) { if (mRealRunner instanceof ClassSkippingTestRunner) { mRealRunner.run(notifier); + RavenwoodAwareTestRunnerHook.onClassSkipped(getDescription()); return; } @@ -294,19 +294,23 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde } private void runWithHooks(Description description, Scope scope, Order order, Statement s) { - Throwable th = null; if (isOnRavenwood()) { Assume.assumeTrue( RavenwoodAwareTestRunnerHook.onBefore(this, description, scope, order)); } try { s.evaluate(); + if (isOnRavenwood()) { + RavenwoodAwareTestRunnerHook.onAfter(this, description, scope, order, null); + } } catch (Throwable t) { - th = t; - SneakyThrow.sneakyThrow(t); - } finally { + boolean shouldThrow = true; if (isOnRavenwood()) { - RavenwoodAwareTestRunnerHook.onAfter(this, description, scope, order, th); + shouldThrow = RavenwoodAwareTestRunnerHook.onAfter( + this, description, scope, order, t); + } + if (shouldThrow) { + SneakyThrow.sneakyThrow(t); } } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java index 6c8d96add4ca..85297fe96d6a 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java @@ -16,37 +16,20 @@ package android.platform.test.ravenwood; -import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED; -import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD; -import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood; -import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode; - -import android.platform.test.annotations.DisabledOnRavenwood; -import android.platform.test.annotations.EnabledOnRavenwood; - -import org.junit.Assume; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; /** - * {@code @ClassRule} that respects Ravenwood-specific class annotations. This rule has no effect - * when tests are run on non-Ravenwood test environments. + * No longer needed. * - * By default, all tests are executed on Ravenwood, but annotations such as - * {@link DisabledOnRavenwood} and {@link EnabledOnRavenwood} can be used at both the method - * and class level to "ignore" tests that may not be ready. + * @deprecated this class used to be used to handle the class level annotation, which + * is now done by the test runner, so this class is not needed. */ +@Deprecated public class RavenwoodClassRule implements TestRule { @Override public Statement apply(Statement base, Description description) { - if (!IS_ON_RAVENWOOD) { - // No check on a real device. - } else if (ENABLE_PROBE_IGNORED) { - Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description)); - } else { - Assume.assumeTrue(shouldEnableOnRavenwood(description)); - } return base; } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 75faafb7fe58..d569896421eb 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -20,22 +20,20 @@ import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.SYSTEM_UID; import static android.os.UserHandle.SYSTEM; -import static org.junit.Assert.fail; +import static com.android.ravenwood.common.RavenwoodCommonUtils.log; +import android.annotation.Nullable; import android.app.Instrumentation; import android.content.Context; import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.EnabledOnRavenwood; -import android.platform.test.annotations.IgnoreUnderRavenwood; import com.android.ravenwood.common.RavenwoodCommonUtils; -import org.junit.Assume; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -56,18 +54,18 @@ import java.util.regex.Pattern; * before a test class is fully initialized. */ public class RavenwoodRule implements TestRule { + private static final String TAG = "RavenwoodRule"; + static final boolean IS_ON_RAVENWOOD = RavenwoodCommonUtils.isOnRavenwood(); /** - * When probing is enabled, all tests will be unconditionally run on Ravenwood to detect + * When this flag is enabled, all tests will be unconditionally run on Ravenwood to detect * cases where a test is able to pass despite being marked as {@link DisabledOnRavenwood}. * * This is typically helpful for internal maintainers discovering tests that had previously * been ignored, but now have enough Ravenwood-supported functionality to be enabled. - * - * TODO: Rename it to a more descriptive name. */ - static final boolean ENABLE_PROBE_IGNORED = "1".equals( + private static final boolean RUN_DISABLED_TESTS = "1".equals( System.getenv("RAVENWOOD_RUN_DISABLED_TESTS")); /** @@ -92,23 +90,17 @@ public class RavenwoodRule implements TestRule { * * Because we use a regex-find, setting "." would disable all tests. */ - private static final Pattern REALLY_DISABLE_PATTERN = Pattern.compile( - Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLE"), "")); - - private static final boolean ENABLE_REALLY_DISABLE_PATTERN = - !REALLY_DISABLE_PATTERN.pattern().isEmpty(); + private static final Pattern REALLY_DISABLED_PATTERN = Pattern.compile( + Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLED"), "")); - /** - * If true, enable optional validation on running tests. - */ - private static final boolean ENABLE_OPTIONAL_VALIDATION = "1".equals( - System.getenv("RAVENWOOD_OPTIONAL_VALIDATION")); + private static final boolean HAS_REALLY_DISABLE_PATTERN = + !REALLY_DISABLED_PATTERN.pattern().isEmpty(); static { - if (ENABLE_PROBE_IGNORED) { - System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests"); - if (ENABLE_REALLY_DISABLE_PATTERN) { - System.out.println("$RAVENWOOD_REALLY_DISABLE=" + REALLY_DISABLE_PATTERN.pattern()); + if (RUN_DISABLED_TESTS) { + log(TAG, "$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests"); + if (HAS_REALLY_DISABLE_PATTERN) { + log(TAG, "$RAVENWOOD_REALLY_DISABLED=" + REALLY_DISABLED_PATTERN.pattern()); } } } @@ -275,103 +267,18 @@ public class RavenwoodRule implements TestRule { "Instrumentation is only available during @Test execution"); } - /** - * Determine if the given {@link Description} should be enabled when running on the - * Ravenwood test environment. - * - * A more specific method-level annotation always takes precedence over any class-level - * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over - * an {@link DisabledOnRavenwood} annotation. - */ - public static boolean shouldEnableOnRavenwood(Description description) { - // First, consult any method-level annotations - if (description.isTest()) { - // Stopgap for http://g/ravenwood/EPAD-N5ntxM - if (description.getMethodName().endsWith("$noRavenwood")) { - return false; - } - if (description.getAnnotation(EnabledOnRavenwood.class) != null) { - return true; - } - if (description.getAnnotation(DisabledOnRavenwood.class) != null) { - return false; - } - if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) { - return false; - } - } - - // Otherwise, consult any class-level annotations - return shouldRunCassOnRavenwood(description.getTestClass()); - } - - public static boolean shouldRunCassOnRavenwood(Class<?> clazz) { - if (clazz != null) { - if (clazz.getAnnotation(EnabledOnRavenwood.class) != null) { - return true; - } - if (clazz.getAnnotation(DisabledOnRavenwood.class) != null) { - return false; - } - if (clazz.getAnnotation(IgnoreUnderRavenwood.class) != null) { - return false; - } - } - return true; - } - - static boolean shouldStillIgnoreInProbeIgnoreMode(Description description) { - if (!ENABLE_REALLY_DISABLE_PATTERN) { - return false; - } - - final var fullname = description.getTestClass().getName() - + (description.isTest() ? "#" + description.getMethodName() : ""); - - if (REALLY_DISABLE_PATTERN.matcher(fullname).find()) { - System.out.println("Still ignoring " + fullname); - return true; - } - return false; - } @Override public Statement apply(Statement base, Description description) { - // No special treatment when running outside Ravenwood; run tests as-is - if (!IS_ON_RAVENWOOD) { - return base; - } - - if (ENABLE_PROBE_IGNORED) { - return applyProbeIgnored(base, description); - } else { - return applyDefault(base, description); - } - } - - private void commonPrologue(Statement base, Description description) throws IOException { - RavenwoodRuleImpl.logTestRunner("started", description); - RavenwoodRuleImpl.validate(base, description, ENABLE_OPTIONAL_VALIDATION); - RavenwoodRuleImpl.init(RavenwoodRule.this); - } - - /** - * Run the given {@link Statement} with no special treatment. - */ - private Statement applyDefault(Statement base, Description description) { + // TODO: Here, we're calling init() / reset() once for each rule. + // That means if a test class has multiple rules -- even if they refer to the same + // rule instance -- we're calling them multiple times. We need to fix it. return new Statement() { @Override public void evaluate() throws Throwable { - Assume.assumeTrue(shouldEnableOnRavenwood(description)); - - commonPrologue(base, description); + RavenwoodRuleImpl.init(RavenwoodRule.this); try { base.evaluate(); - - RavenwoodRuleImpl.logTestRunner("finished", description); - } catch (Throwable t) { - RavenwoodRuleImpl.logTestRunner("failed", description); - throw t; } finally { RavenwoodRuleImpl.reset(RavenwoodRule.this); } @@ -380,44 +287,6 @@ public class RavenwoodRule implements TestRule { } /** - * Run the given {@link Statement} with probing enabled. All tests will be unconditionally - * run on Ravenwood to detect cases where a test is able to pass despite being marked as - * {@code IgnoreUnderRavenwood}. - */ - private Statement applyProbeIgnored(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description)); - - commonPrologue(base, description); - try { - base.evaluate(); - } catch (Throwable t) { - // If the test isn't included, eat the exception and report the - // assumption failure that test authors expect; otherwise throw - Assume.assumeTrue(shouldEnableOnRavenwood(description)); - throw t; - } finally { - RavenwoodRuleImpl.logTestRunner("finished", description); - RavenwoodRuleImpl.reset(RavenwoodRule.this); - } - - if (!shouldEnableOnRavenwood(description)) { - fail("Test wasn't included under Ravenwood, but it actually " - + "passed under Ravenwood; consider updating annotations"); - } - } - }; - } - - public static class _$RavenwoodPrivate { - public static boolean isOptionalValidationEnabled() { - return ENABLE_OPTIONAL_VALIDATION; - } - } - - /** * Returns the "real" result from {@link System#currentTimeMillis()}. * * Currently, it's the same thing as calling {@link System#currentTimeMillis()}, @@ -427,4 +296,47 @@ public class RavenwoodRule implements TestRule { public long realCurrentTimeMillis() { return System.currentTimeMillis(); } + + // Below are internal to ravenwood. Don't use them from normal tests... + + public static class RavenwoodPrivate { + private RavenwoodPrivate() { + } + + private volatile Boolean mRunDisabledTestsOverride = null; + + private volatile Pattern mReallyDisabledPattern = null; + + public boolean isRunningDisabledTests() { + if (mRunDisabledTestsOverride != null) { + return mRunDisabledTestsOverride; + } + return RUN_DISABLED_TESTS; + } + + public Pattern getReallyDisabledPattern() { + if (mReallyDisabledPattern != null) { + return mReallyDisabledPattern; + } + return REALLY_DISABLED_PATTERN; + } + + public void overrideRunDisabledTest(boolean runDisabledTests, + @Nullable String reallyDisabledPattern) { + mRunDisabledTestsOverride = runDisabledTests; + mReallyDisabledPattern = + reallyDisabledPattern == null ? null : Pattern.compile(reallyDisabledPattern); + } + + public void resetRunDisabledTest() { + mRunDisabledTestsOverride = null; + mReallyDisabledPattern = null; + } + } + + private static final RavenwoodPrivate sRavenwoodPrivate = new RavenwoodPrivate(); + + public static RavenwoodPrivate private$ravenwood() { + return sRavenwoodPrivate; + } } diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java index 6b80e0cbf91e..1e4889ce0678 100644 --- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java +++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -23,27 +23,51 @@ import org.junit.runner.Runner; import org.junit.runners.model.TestClass; /** - * Provide hook points created by {@link RavenwoodAwareTestRunner}. + * Provide hook points created by {@link RavenwoodAwareTestRunner}. This is a version + * that's used on a device side test. + * + * All methods are no-op in real device tests. + * + * TODO: Use some kind of factory to provide different implementation for the device test + * and the ravenwood test. */ public class RavenwoodAwareTestRunnerHook { private RavenwoodAwareTestRunnerHook() { } /** - * Called when a runner starts, befre the inner runner gets a chance to run. + * Called when a runner starts, before the inner runner gets a chance to run. */ public static void onRunnerInitializing(Runner runner, TestClass testClass) { - // No-op on a real device. } + /** + * Called when a whole test class is skipped. + */ + public static void onClassSkipped(Description description) { + } + + /** + * Called before a test / class. + * + * Return false if it should be skipped. + */ public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description, Scope scope, Order order) { - // No-op on a real device. return true; } - public static void onAfter(RavenwoodAwareTestRunner runner, Description description, + /** + * Called after a test / class. + * + * Return false if the exception should be ignored. + */ + public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description, Scope scope, Order order, Throwable th) { - // No-op on a real device. + return true; + } + + public static boolean shouldRunClassOnRavenwood(Class<?> clazz) { + return true; } } diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 483b98a96034..a470626dcbe7 100644 --- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -17,7 +17,6 @@ package android.platform.test.ravenwood; import org.junit.runner.Description; -import org.junit.runners.model.Statement; public class RavenwoodRuleImpl { public static void init(RavenwoodRule rule) { @@ -32,10 +31,6 @@ public class RavenwoodRuleImpl { // No-op when running on a real device } - public static void validate(Statement base, Description description, - boolean enableOptionalValidation) { - } - public static long realCurrentTimeMillis() { return System.currentTimeMillis(); } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 8e2e0ad76d15..08cc9c33357b 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -121,6 +121,13 @@ flag { } flag { + name: "enable_magnification_keyboard_control" + namespace: "accessibility" + description: "Whether to enable keyboard control for magnification" + bug: "355487062" +} + +flag { name: "fix_drag_pointer_when_ending_drag" namespace: "accessibility" description: "Send the correct pointer id when transitioning from dragging to delegating states." diff --git a/services/companion/Android.bp b/services/companion/Android.bp index 2bfdd0a7c819..77650ebe2698 100644 --- a/services/companion/Android.bp +++ b/services/companion/Android.bp @@ -28,7 +28,6 @@ java_library_static { ], static_libs: [ "ukey2_jni", - "virtualdevice_flags_lib", "virtual_camera_service_aidl-java", ], lint: { diff --git a/services/companion/java/com/android/server/companion/virtual/Android.bp b/services/companion/java/com/android/server/companion/virtual/Android.bp deleted file mode 100644 index 66313e6ca957..000000000000 --- a/services/companion/java/com/android/server/companion/virtual/Android.bp +++ /dev/null @@ -1,17 +0,0 @@ -package { - default_team: "trendy_team_xr_framework", -} - -java_aconfig_library { - name: "virtualdevice_flags_lib", - aconfig_declarations: "virtualdevice_flags", -} - -aconfig_declarations { - name: "virtualdevice_flags", - package: "com.android.server.companion.virtual", - container: "system", - srcs: [ - "flags.aconfig", - ], -} diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceLog.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceLog.java index b0bacfd158ed..fed153f0b7fa 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceLog.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceLog.java @@ -48,9 +48,6 @@ final class VirtualDeviceLog { void logCreated(int deviceId, int ownerUid) { final long token = Binder.clearCallingIdentity(); try { - if (!Flags.dumpHistory()) { - return; - } addEntry(new LogEntry(TYPE_CREATED, deviceId, System.currentTimeMillis(), ownerUid)); } finally { Binder.restoreCallingIdentity(token); @@ -60,9 +57,6 @@ final class VirtualDeviceLog { void logClosed(int deviceId, int ownerUid) { final long token = Binder.clearCallingIdentity(); try { - if (!Flags.dumpHistory()) { - return; - } addEntry(new LogEntry(TYPE_CLOSED, deviceId, System.currentTimeMillis(), ownerUid)); } finally { Binder.restoreCallingIdentity(token); @@ -79,9 +73,6 @@ final class VirtualDeviceLog { void dump(PrintWriter pw) { final long token = Binder.clearCallingIdentity(); try { - if (!Flags.dumpHistory()) { - return; - } pw.println("VirtualDevice Log:"); UidToPackageNameCache packageNameCache = new UidToPackageNameCache( mContext.getPackageManager()); diff --git a/services/companion/java/com/android/server/companion/virtual/flags.aconfig b/services/companion/java/com/android/server/companion/virtual/flags.aconfig deleted file mode 100644 index 616f5d09e13f..000000000000 --- a/services/companion/java/com/android/server/companion/virtual/flags.aconfig +++ /dev/null @@ -1,11 +0,0 @@ -# OLD PACKAGE, DO NOT USE: Prefer `flags.aconfig` in core/java/android/companion/virtual -# (or other custom files) to define your flags -package: "com.android.server.companion.virtual" -container: "system" - -flag { - name: "dump_history" - namespace: "virtual_devices" - description: "This flag controls if a history of virtual devices is shown in dumpsys virtualdevices" - bug: "293114719" -}
\ No newline at end of file diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags index 361b818260f1..fd512a64b32c 100644 --- a/services/core/java/com/android/server/EventLogTags.logtags +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -94,6 +94,8 @@ option java_package com.android.server 275534 notification_unautogrouped (key|3) # when a notification is adjusted via assistant 27535 notification_adjusted (key|3),(adjustment_type|3),(new_value|3) +# when a notification cancellation is prevented by the system +27536 notification_cancel_prevented (key|3) # --------------------------- # Watchdog.java diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d80b38e32b6c..d12153559b31 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -16248,43 +16248,55 @@ public class ActivityManagerService extends IActivityManager.Stub boolean closeFd = true; try { - synchronized (mProcLock) { - if (fd == null) { - throw new IllegalArgumentException("null fd"); - } - mBinderTransactionTrackingEnabled = false; + Objects.requireNonNull(fd); + + record ProcessToDump(String processName, IApplicationThread thread) { } - PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd.getFileDescriptor())); - pw.println("Binder transaction traces for all processes.\n"); - mProcessList.forEachLruProcessesLOSP(true, process -> { + PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd.getFileDescriptor())); + pw.println("Binder transaction traces for all processes.\n"); + final ArrayList<ProcessToDump> processes = new ArrayList<>(); + synchronized (mProcLock) { + // Since dumping binder transactions is a long-running operation, we can't do it + // with mProcLock held. Do the initial verification here, and save the processes + // to dump later outside the lock. + final ArrayList<ProcessRecord> unverifiedProcesses = + new ArrayList<>(mProcessList.getLruProcessesLOSP()); + for (int i = 0, size = unverifiedProcesses.size(); i < size; i++) { + ProcessRecord process = unverifiedProcesses.get(i); final IApplicationThread thread = process.getThread(); if (!processSanityChecksLPr(process, thread)) { - return; + continue; } + processes.add(new ProcessToDump(process.processName, process.getThread())); + } + mBinderTransactionTrackingEnabled = false; + } + for (int i = 0, size = processes.size(); i < size; i++) { + final String processName = processes.get(i).processName(); + final IApplicationThread thread = processes.get(i).thread(); - pw.println("Traces for process: " + process.processName); - pw.flush(); + pw.println("Traces for process: " + processName); + pw.flush(); + try { + TransferPipe tp = new TransferPipe(); try { - TransferPipe tp = new TransferPipe(); - try { - thread.stopBinderTrackingAndDump(tp.getWriteFd()); - tp.go(fd.getFileDescriptor()); - } finally { - tp.kill(); - } - } catch (IOException e) { - pw.println("Failure while dumping IPC traces from " + process + - ". Exception: " + e); - pw.flush(); - } catch (RemoteException e) { - pw.println("Got a RemoteException while dumping IPC traces from " + - process + ". Exception: " + e); - pw.flush(); + thread.stopBinderTrackingAndDump(tp.getWriteFd()); + tp.go(fd.getFileDescriptor()); + } finally { + tp.kill(); } - }); - closeFd = false; - return true; + } catch (IOException e) { + pw.println("Failure while dumping IPC traces from " + processName + + ". Exception: " + e); + pw.flush(); + } catch (RemoteException e) { + pw.println("Got a RemoteException while dumping IPC traces from " + + processName + ". Exception: " + e); + pw.flush(); + } } + closeFd = false; + return true; } finally { if (fd != null && closeFd) { try { diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index ab63e247b556..0e266f5644b6 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1201,6 +1201,7 @@ public class OomAdjuster { >= UNKNOWN_ADJ) { final ProcessServiceRecord psr = app.mServices; switch (state.getCurProcState()) { + case PROCESS_STATE_LAST_ACTIVITY: case PROCESS_STATE_CACHED_ACTIVITY: case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT: case ActivityManager.PROCESS_STATE_CACHED_RECENT: @@ -2180,7 +2181,6 @@ public class OomAdjuster { procState = PROCESS_STATE_LAST_ACTIVITY; schedGroup = SCHED_GROUP_BACKGROUND; state.setAdjType("previous-expired"); - adj = CACHED_APP_MIN_ADJ; if (DEBUG_OOM_ADJ_REASON || logUid == appUid) { reportOomAdjMessageLocked(TAG_OOM_ADJ, "Expire prev adj: " + app); } diff --git a/services/core/java/com/android/server/appop/TEST_MAPPING b/services/core/java/com/android/server/appop/TEST_MAPPING index 2a9dfa273e87..9317c1eda088 100644 --- a/services/core/java/com/android/server/appop/TEST_MAPPING +++ b/services/core/java/com/android/server/appop/TEST_MAPPING @@ -18,24 +18,7 @@ "name": "FrameworksMockingServicesTests_android_server_appop" }, { - "name": "CtsPermissionTestCases", - "options": [ - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "include-filter": "android.permission.cts.BackgroundPermissionsTest" - }, - { - "include-filter": "android.permission.cts.SplitPermissionTest" - }, - { - "include-filter": "android.permission.cts.PermissionFlagsTest" - }, - { - "include-filter": "android.permission.cts.SharedUidPermissionsTest" - } - ] + "name": "CtsPermissionTestCases_Platform" }, { "name": "CtsAppTestCases", diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index 276ab03d9f4b..abfbddc18e24 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -221,7 +221,8 @@ public final class AuthSession implements IBinder.DeathRecipient { mFingerprintSensorProperties = fingerprintSensorProperties; mCancelled = false; mBiometricFrameworkStatsLogger = logger; - mOperationContext = new OperationContextExt(true /* isBP */); + mOperationContext = new OperationContextExt(true /* isBP */, + preAuthInfo.getIsMandatoryBiometricsAuthentication() /* isMandatoryBiometrics */); mBiometricManager = mContext.getSystemService(BiometricManager.class); mSfpsSensorIds = mFingerprintSensorProperties.stream().filter( @@ -285,7 +286,8 @@ public final class AuthSession implements IBinder.DeathRecipient { sensor.goToStateWaitingForCookie(requireConfirmation, mToken, mOperationId, mUserId, mSensorReceiver, mOpPackageName, mRequestId, cookie, mPromptInfo.isAllowBackgroundAuthentication(), - mPromptInfo.isForLegacyFingerprintManager()); + mPromptInfo.isForLegacyFingerprintManager(), + mOperationContext.getIsMandatoryBiometrics()); } } diff --git a/services/core/java/com/android/server/biometrics/BiometricSensor.java b/services/core/java/com/android/server/biometrics/BiometricSensor.java index 42dd36a5c35a..c7532d44fd38 100644 --- a/services/core/java/com/android/server/biometrics/BiometricSensor.java +++ b/services/core/java/com/android/server/biometrics/BiometricSensor.java @@ -107,12 +107,13 @@ public abstract class BiometricSensor { void goToStateWaitingForCookie(boolean requireConfirmation, IBinder token, long sessionId, int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId, int cookie, boolean allowBackgroundAuthentication, - boolean isForLegacyFingerprintManager) + boolean isForLegacyFingerprintManager, boolean isMandatoryBiometrics) throws RemoteException { mCookie = cookie; impl.prepareForAuthentication(requireConfirmation, token, sessionId, userId, sensorReceiver, opPackageName, requestId, mCookie, - allowBackgroundAuthentication, isForLegacyFingerprintManager); + allowBackgroundAuthentication, isForLegacyFingerprintManager, + isMandatoryBiometrics); mSensorState = STATE_WAITING_FOR_COOKIE; } diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index f0da67bd9f9a..eaf4f813d13a 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -447,6 +447,12 @@ class PreAuthInfo { getInternalStatus().second)); } + /** Returns if mandatory biometrics authentication is in effect */ + boolean getIsMandatoryBiometricsAuthentication() { + return mIsMandatoryBiometricsAuthentication; + } + + /** * For the given request, generate the appropriate reason why authentication cannot be started. * Note that for some errors, modality is intentionally cleared. diff --git a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java index 2c52e3d8ca67..f5af5ea3eab4 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricFrameworkStatsLogger.java @@ -78,6 +78,7 @@ public class BiometricFrameworkStatsLogger { public void authenticate(OperationContextExt operationContext, int statsModality, int statsAction, int statsClient, boolean isDebug, long latency, int authState, boolean requireConfirmation, int targetUserId, float ambientLightLux) { + Slog.d(TAG, "authenticate logging " + operationContext.getIsMandatoryBiometrics()); FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_AUTHENTICATED, statsModality, targetUserId, @@ -98,7 +99,8 @@ public class BiometricFrameworkStatsLogger { foldType(operationContext.getFoldState()), operationContext.getOrderAndIncrement(), toProtoWakeReason(operationContext), - toProtoWakeReasonDetails(operationContext)); + toProtoWakeReasonDetails(operationContext), + operationContext.getIsMandatoryBiometrics()); } /** {@see FrameworkStatsLog.BIOMETRIC_AUTHENTICATED}. */ @@ -129,6 +131,7 @@ public class BiometricFrameworkStatsLogger { public void error(OperationContextExt operationContext, int statsModality, int statsAction, int statsClient, boolean isDebug, long latency, int error, int vendorCode, int targetUserId) { + Slog.d(TAG, "error logging " + operationContext.getIsMandatoryBiometrics()); FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_ERROR_OCCURRED, statsModality, targetUserId, @@ -149,7 +152,8 @@ public class BiometricFrameworkStatsLogger { foldType(operationContext.getFoldState()), operationContext.getOrderAndIncrement(), toProtoWakeReason(operationContext), - toProtoWakeReasonDetails(operationContext)); + toProtoWakeReasonDetails(operationContext), + operationContext.getIsMandatoryBiometrics()); } @VisibleForTesting diff --git a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java index da4e5154d49e..4df63e2729d2 100644 --- a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java +++ b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java @@ -50,21 +50,33 @@ public class OperationContextExt { @Surface.Rotation private int mOrientation = Surface.ROTATION_0; private int mFoldState = IBiometricContextListener.FoldState.UNKNOWN; private final boolean mIsBP; + private final boolean mIsMandatoryBiometrics; /** Create a context. */ public OperationContextExt(boolean isBP) { this(new OperationContext(), isBP, BiometricAuthenticator.TYPE_NONE); } - public OperationContextExt(boolean isBP, @BiometricAuthenticator.Modality int modality) { - this(new OperationContext(), isBP, modality); + public OperationContextExt(boolean isBP, boolean isMandatoryBiometrics) { + this(new OperationContext(), isBP, BiometricAuthenticator.TYPE_NONE, isMandatoryBiometrics); + } + + public OperationContextExt(boolean isBP, @BiometricAuthenticator.Modality int modality, + boolean isMandatoryBiometrics) { + this(new OperationContext(), isBP, modality, isMandatoryBiometrics); } /** Create a wrapped context. */ public OperationContextExt(@NonNull OperationContext context, boolean isBP, @BiometricAuthenticator.Modality int modality) { + this(context, isBP, modality, false /* isMandatoryBiometrics */); + } + + public OperationContextExt(@NonNull OperationContext context, boolean isBP, + @BiometricAuthenticator.Modality int modality, boolean isMandatoryBiometrics) { mAidlContext = context; mIsBP = isBP; + mIsMandatoryBiometrics = isMandatoryBiometrics; if (modality == BiometricAuthenticator.TYPE_FINGERPRINT) { mAidlContext.operationState = OperationState.fingerprintOperationState( @@ -285,6 +297,11 @@ public class OperationContextExt { return mAidlContext.operationState; } + /** If mandatory biometrics is active. */ + public boolean getIsMandatoryBiometrics() { + return mIsMandatoryBiometrics; + } + /** Update this object with the latest values from the given context. */ OperationContextExt update(@NonNull BiometricContext biometricContext, boolean isCrypto) { mAidlContext.isAod = biometricContext.isAod(); diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java index fbd32a67fe6c..04522e367272 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java @@ -59,9 +59,10 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement public AcquisitionClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon, @NonNull IBinder token, @NonNull ClientMonitorCallbackConverter listener, int userId, @NonNull String owner, int cookie, int sensorId, boolean shouldVibrate, - @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext) { + @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, + boolean isMandatoryBiometrics) { super(context, lazyDaemon, token, listener, userId, owner, cookie, sensorId, - logger, biometricContext); + logger, biometricContext, isMandatoryBiometrics); mPowerManager = context.getSystemService(PowerManager.class); mShouldVibrate = shouldVibrate; } diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index daaafcb61bc5..09386ae2899d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -99,7 +99,7 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> boolean shouldVibrate, int sensorStrength) { super(context, lazyDaemon, token, listener, options.getUserId(), options.getOpPackageName(), cookie, options.getSensorId(), shouldVibrate, - biometricLogger, biometricContext); + biometricLogger, biometricContext, options.isMandatoryBiometrics()); mIsStrongBiometric = isStrongBiometric; mOperationId = operationId; mRequireConfirmation = requireConfirmation; diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java index 438367d54e1f..32c0bd389e2f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java @@ -60,7 +60,7 @@ public abstract class EnrollClient<T> extends AcquisitionClient<T> implements En @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, int enrollReason) { super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, - shouldVibrate, logger, biometricContext); + shouldVibrate, logger, biometricContext, false /* isMandatoryBiometrics */); mBiometricUtils = utils; mHardwareAuthToken = Arrays.copyOf(hardwareAuthToken, hardwareAuthToken.length); mTimeoutSec = timeoutSec; diff --git a/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java index 2adf0cb3bab4..b573b56626aa 100644 --- a/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/GenerateChallengeClient.java @@ -37,7 +37,7 @@ public abstract class GenerateChallengeClient<T> extends HalClientMonitor<T> { int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext) { super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, - biometricLogger, biometricContext); + biometricLogger, biometricContext, false /* isMandatoryBiometrics */); } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java index 0f01510bd908..3bc51a94cd2f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java @@ -41,26 +41,29 @@ public abstract class HalClientMonitor<T> extends BaseClientMonitor { private final OperationContextExt mOperationContext; /** - * @param context system_server context - * @param lazyDaemon pointer for lazy retrieval of the HAL - * @param token a unique token for the client - * @param listener recipient of related events (e.g. authentication) - * @param userId target user id for operation - * @param owner name of the client that owns this - * @param cookie BiometricPrompt authentication cookie (to be moved into a subclass soon) - * @param sensorId ID of the sensor that the operation should be requested of - * @param biometricLogger framework stats logger + * @param context system_server context + * @param lazyDaemon pointer for lazy retrieval of the HAL + * @param token a unique token for the client + * @param listener recipient of related events (e.g. authentication) + * @param userId target user id for operation + * @param owner name of the client that owns this + * @param cookie BiometricPrompt authentication cookie (to be moved into a subclass + * soon) + * @param sensorId ID of the sensor that the operation should be requested of + * @param biometricLogger framework stats logger * @param biometricContext system context metadata */ public HalClientMonitor(@NonNull Context context, @NonNull Supplier<T> lazyDaemon, @Nullable IBinder token, @Nullable ClientMonitorCallbackConverter listener, int userId, @NonNull String owner, int cookie, int sensorId, - @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext) { + @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext, + boolean isMandatoryBiometrics) { super(context, token, listener, userId, owner, cookie, sensorId, biometricLogger, biometricContext); mLazyDaemon = lazyDaemon; int modality = listener != null ? listener.getModality() : BiometricAuthenticator.TYPE_NONE; - mOperationContext = new OperationContextExt(isBiometricPrompt(), modality); + mOperationContext = new OperationContextExt(isBiometricPrompt(), modality, + isMandatoryBiometrics); } @Nullable diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java index 7bd905b4524a..6c305599c39f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java @@ -143,7 +143,8 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide @NonNull BiometricUtils<S> utils, @NonNull Map<Integer, Long> authenticatorIds) { super(context, lazyDaemon, null /* token */, null /* ClientMonitorCallbackConverter */, - userId, owner, 0 /* cookie */, sensorId, logger, biometricContext); + userId, owner, 0 /* cookie */, sensorId, logger, biometricContext, + false /* isMandatoryBiometrics */); mBiometricUtils = utils; mAuthenticatorIds = authenticatorIds; mHasEnrollmentsBeforeStarting = !utils.getBiometricsForUser(context, userId).isEmpty(); diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java index 81ab26dd581e..2c2c685fd442 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java @@ -55,7 +55,8 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T> // Internal enumerate does not need to send results to anyone. Cleanup (enumerate + remove) // is all done internally. super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner, - 0 /* cookie */, sensorId, logger, biometricContext); + 0 /* cookie */, sensorId, logger, biometricContext, + false /* isMandatoryBiometrics */); mEnrolledList = enrolledList; mInitialEnrolledSize = mEnrolledList.size(); mUtils = utils; diff --git a/services/core/java/com/android/server/biometrics/sensors/InvalidationClient.java b/services/core/java/com/android/server/biometrics/sensors/InvalidationClient.java index d5aa5e2c9db6..6c933665a2e2 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InvalidationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InvalidationClient.java @@ -49,7 +49,7 @@ public abstract class InvalidationClient<S extends BiometricAuthenticator.Identi @NonNull IInvalidationCallback callback) { super(context, lazyDaemon, null /* token */, null /* listener */, userId, context.getOpPackageName(), 0 /* cookie */, sensorId, - logger, biometricContext); + logger, biometricContext, false /* isMandatoryBiometrics */); mAuthenticatorIds = authenticatorIds; mInvalidationCallback = callback; } diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java index d2ef2786c252..ad5877aed6da 100644 --- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java @@ -49,7 +49,7 @@ public abstract class RemovalClient<S extends BiometricAuthenticator.Identifier, @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, @NonNull Map<Integer, Long> authenticatorIds) { super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, - logger, biometricContext); + logger, biometricContext, false /* isMandatoryBiometrics */); mBiometricUtils = utils; mAuthenticatorIds = authenticatorIds; mHasEnrollmentsBeforeStarting = !utils.getBiometricsForUser(context, userId).isEmpty(); diff --git a/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java index 88f4da261d62..0c8a2ddffc57 100644 --- a/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/RevokeChallengeClient.java @@ -32,7 +32,8 @@ public abstract class RevokeChallengeClient<T> extends HalClientMonitor<T> { @NonNull IBinder token, int userId, @NonNull String owner, int sensorId, @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext) { super(context, lazyDaemon, token, null /* listener */, userId, owner, - 0 /* cookie */, sensorId, biometricLogger, biometricContext); + 0 /* cookie */, sensorId, biometricLogger, biometricContext, + false /* isMandatoryBiometrics */); } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/StartUserClient.java b/services/core/java/com/android/server/biometrics/sensors/StartUserClient.java index 21c9f64eea5b..ff694cda8812 100644 --- a/services/core/java/com/android/server/biometrics/sensors/StartUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/StartUserClient.java @@ -51,7 +51,8 @@ public abstract class StartUserClient<T, U> extends HalClientMonitor<T> { @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, @NonNull UserStartedCallback<U> callback) { super(context, lazyDaemon, token, null /* listener */, userId, context.getOpPackageName(), - 0 /* cookie */, sensorId, logger, biometricContext); + 0 /* cookie */, sensorId, logger, biometricContext, + false /* isMandatoryBiometrics */); mUserStartedCallback = callback; } diff --git a/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java index e01c4ec76ed2..9119bc72fdd3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/StopUserClient.java @@ -54,7 +54,8 @@ public abstract class StopUserClient<T> extends HalClientMonitor<T> { @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, @NonNull UserStoppedCallback callback) { super(context, lazyDaemon, token, null /* listener */, userId, context.getOpPackageName(), - 0 /* cookie */, sensorId, logger, biometricContext); + 0 /* cookie */, sensorId, logger, biometricContext, + false /* isMandatoryBiometrics */); mUserStoppedCallback = callback; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java index 22110037890f..67d75052c8d6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceAuthenticator.java @@ -63,13 +63,14 @@ public final class FaceAuthenticator extends IBiometricAuthenticator.Stub { public void prepareForAuthentication(boolean requireConfirmation, IBinder token, long operationId, int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId, int cookie, boolean allowBackgroundAuthentication, - boolean isForLegacyFingerprintManager) + boolean isForLegacyFingerprintManager, boolean isMandatoryBiometrics) throws RemoteException { mFaceService.prepareForAuthentication(requireConfirmation, token, operationId, sensorReceiver, new FaceAuthenticateOptions.Builder() .setUserId(userId) .setSensorId(mSensorId) .setOpPackageName(opPackageName) + .setIsMandatoryBiometrics(isMandatoryBiometrics) .build(), requestId, cookie, allowBackgroundAuthentication); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java index 8b4da3181bcf..8eb62eb114bd 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java @@ -83,7 +83,8 @@ public class FaceDetectClient extends AcquisitionClient<AidlSession> implements boolean isStrongBiometric, SensorPrivacyManager sensorPrivacyManager) { super(context, lazyDaemon, token, listener, options.getUserId(), options.getOpPackageName(), 0 /* cookie */, options.getSensorId(), - false /* shouldVibrate */, logger, biometricContext); + false /* shouldVibrate */, logger, biometricContext, + false /* isMandatoryBiometrics */); setRequestId(requestId); mAuthenticationStateListeners = authenticationStateListeners; mIsStrongBiometric = isStrongBiometric; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetAuthenticatorIdClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetAuthenticatorIdClient.java index 1f4f6127dafd..dbd293cd9f8d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetAuthenticatorIdClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetAuthenticatorIdClient.java @@ -42,7 +42,8 @@ class FaceGetAuthenticatorIdClient extends HalClientMonitor<AidlSession> { @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, Map<Integer, Long> authenticatorIds) { super(context, lazyDaemon, null /* token */, null /* listener */, userId, opPackageName, - 0 /* cookie */, sensorId, logger, biometricContext); + 0 /* cookie */, sensorId, logger, biometricContext, + false /* isMandatoryBiometrics */); mAuthenticatorIds = authenticatorIds; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java index c41b706555e8..8d1336f21ccf 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGetFeatureClient.java @@ -55,7 +55,7 @@ public class FaceGetFeatureClient extends HalClientMonitor<AidlSession> implemen @NonNull String owner, int sensorId, @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, int feature) { super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, - logger, biometricContext); + logger, biometricContext, false /* isMandatoryBiometrics */); mUserId = userId; mFeature = feature; } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java index d02eefaed101..93bc1f263678 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java @@ -60,7 +60,8 @@ public class FaceResetLockoutClient extends HalClientMonitor<AidlSession> implem @NonNull LockoutResetDispatcher lockoutResetDispatcher, @Authenticators.Types int biometricStrength) { super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, - 0 /* cookie */, sensorId, logger, biometricContext); + 0 /* cookie */, sensorId, logger, biometricContext, + false /* isMandatoryBiometrics */); mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken); mLockoutTracker = lockoutTracker; mLockoutResetDispatcher = lockoutResetDispatcher; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceSetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceSetFeatureClient.java index f6da8726564f..fc73e58ba577 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceSetFeatureClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceSetFeatureClient.java @@ -52,7 +52,7 @@ public class FaceSetFeatureClient extends HalClientMonitor<AidlSession> implemen @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, int feature, boolean enabled, byte[] hardwareAuthToken) { super(context, lazyDaemon, token, listener, userId, owner, 0 /* cookie */, sensorId, - logger, biometricContext); + logger, biometricContext, false /* isMandatoryBiometrics */); mFeature = feature; mEnabled = enabled; mHardwareAuthToken = HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java index b6fa0c126cd6..d50ab8d73e3e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintAuthenticator.java @@ -63,13 +63,14 @@ public final class FingerprintAuthenticator extends IBiometricAuthenticator.Stub public void prepareForAuthentication(boolean requireConfirmation, IBinder token, long operationId, int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId, int cookie, boolean allowBackgroundAuthentication, - boolean isForLegacyFingerprintManager) + boolean isForLegacyFingerprintManager, boolean isMandatoryBiometrics) throws RemoteException { mFingerprintService.prepareForAuthentication(token, operationId, sensorReceiver, new FingerprintAuthenticateOptions.Builder() .setSensorId(mSensorId) .setUserId(userId) .setOpPackageName(opPackageName) + .setIsMandatoryBiometrics(isMandatoryBiometrics) .build(), requestId, cookie, allowBackgroundAuthentication, isForLegacyFingerprintManager); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java index fb48053913cf..a81ab8ef4c22 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java @@ -71,7 +71,8 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession> boolean isStrongBiometric) { super(context, lazyDaemon, token, listener, options.getUserId(), options.getOpPackageName(), 0 /* cookie */, options.getSensorId(), - true /* shouldVibrate */, biometricLogger, biometricContext); + true /* shouldVibrate */, biometricLogger, biometricContext, + false /* isMandatoryBiometrics */); setRequestId(requestId); mAuthenticationStateListeners = authenticationStateListeners; mIsStrongBiometric = isStrongBiometric; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java index 03539690c0a8..f77e11600502 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGetAuthenticatorIdClient.java @@ -42,7 +42,8 @@ public class FingerprintGetAuthenticatorIdClient extends HalClientMonitor<AidlSe @NonNull BiometricLogger biometricLogger, @NonNull BiometricContext biometricContext, Map<Integer, Long> authenticatorIds) { super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, - 0 /* cookie */, sensorId, biometricLogger, biometricContext); + 0 /* cookie */, sensorId, biometricLogger, biometricContext, + false /* isMandatoryBiometrics */); mAuthenticatorIds = authenticatorIds; } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java index 387ae12147ae..81a3d8394435 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java @@ -59,7 +59,8 @@ public class FingerprintResetLockoutClient extends HalClientMonitor<AidlSession> @NonNull LockoutResetDispatcher lockoutResetDispatcher, @Authenticators.Types int biometricStrength) { super(context, lazyDaemon, null /* token */, null /* listener */, userId, owner, - 0 /* cookie */, sensorId, biometricLogger, biometricContext); + 0 /* cookie */, sensorId, biometricLogger, biometricContext, + false /* isMandatoryBiometrics */); mHardwareAuthToken = hardwareAuthToken == null ? null : HardwareAuthTokenUtils.toHardwareAuthToken(hardwareAuthToken); mLockoutCache = lockoutTracker; diff --git a/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java b/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java index 01d1e378a735..76b5c4e8f310 100644 --- a/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java +++ b/services/core/java/com/android/server/biometrics/sensors/iris/IrisAuthenticator.java @@ -60,7 +60,7 @@ public final class IrisAuthenticator extends IBiometricAuthenticator.Stub { public void prepareForAuthentication(boolean requireConfirmation, IBinder token, long sessionId, int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId, int cookie, boolean allowBackgroundAuthentication, - boolean isForLegacyFingerprintManager) + boolean isForLegacyFingerprintManager, boolean isMandatoryBiometrics) throws RemoteException { } diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java index d0b8990e37c4..f44b85273af6 100644 --- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java +++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java @@ -142,7 +142,6 @@ class RebootEscrowManager { ERROR_KEYSTORE_FAILURE, ERROR_NO_NETWORK, ERROR_TIMEOUT_EXHAUSTED, - ERROR_NO_REBOOT_ESCROW_DATA, }) @Retention(RetentionPolicy.SOURCE) @interface RebootEscrowErrorCode { @@ -158,7 +157,6 @@ class RebootEscrowManager { static final int ERROR_KEYSTORE_FAILURE = 7; static final int ERROR_NO_NETWORK = 8; static final int ERROR_TIMEOUT_EXHAUSTED = 9; - static final int ERROR_NO_REBOOT_ESCROW_DATA = 10; private @RebootEscrowErrorCode int mLoadEscrowDataErrorCode = ERROR_NONE; @@ -507,9 +505,6 @@ class RebootEscrowManager { if (rebootEscrowUsers.isEmpty()) { Slog.i(TAG, "No reboot escrow data found for users," + " skipping loading escrow data"); - setLoadEscrowDataErrorCode(ERROR_NO_REBOOT_ESCROW_DATA, retryHandler); - reportMetricOnRestoreComplete( - /* success= */ false, /* attemptCount= */ 1, retryHandler); clearMetricsStorage(); return; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 54bc6fbfd930..dbe778e4d971 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -11777,6 +11777,8 @@ public class NotificationManagerService extends SystemService { mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(), record, isAppForeground, /* isAppProvided= */ false, tracker)); + + EventLogTags.writeNotificationCancelPrevented(record.getKey()); } } diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 95e5b84a8ed3..2bc6d53147fb 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -503,26 +503,21 @@ public class UserRestrictionsUtils { String restriction, boolean isMainUser, boolean isProfileOwnerOnOrgOwnedDevice) { - if (android.app.admin.flags.Flags.esimManagementEnabled()) { - if (IMMUTABLE_BY_OWNERS.contains(restriction)) { - return false; - } - if (DEVICE_OWNER_ONLY_RESTRICTIONS.contains(restriction)) { - return false; - } - if (!isMainUser && MAIN_USER_ONLY_RESTRICTIONS.contains(restriction)) { - return false; - } - if (!isProfileOwnerOnOrgOwnedDevice - && PROFILE_OWNER_ORGANIZATION_OWNED_PROFILE_RESTRICTIONS.contains( - restriction)) { - return false; - } - return true; + if (IMMUTABLE_BY_OWNERS.contains(restriction)) { + return false; + } + if (DEVICE_OWNER_ONLY_RESTRICTIONS.contains(restriction)) { + return false; + } + if (!isMainUser && MAIN_USER_ONLY_RESTRICTIONS.contains(restriction)) { + return false; + } + if (!isProfileOwnerOnOrgOwnedDevice + && PROFILE_OWNER_ORGANIZATION_OWNED_PROFILE_RESTRICTIONS.contains( + restriction)) { + return false; } - return !IMMUTABLE_BY_OWNERS.contains(restriction) - && !DEVICE_OWNER_ONLY_RESTRICTIONS.contains(restriction) - && !(!isMainUser && MAIN_USER_ONLY_RESTRICTIONS.contains(restriction)); + return true; } /** diff --git a/services/core/java/com/android/server/pm/permission/TEST_MAPPING b/services/core/java/com/android/server/pm/permission/TEST_MAPPING index 24323c8bfbde..8a3c74b4879c 100644 --- a/services/core/java/com/android/server/pm/permission/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/permission/TEST_MAPPING @@ -1,24 +1,7 @@ { "presubmit": [ { - "name": "CtsPermissionTestCases", - "options": [ - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "include-filter": "android.permission.cts.BackgroundPermissionsTest" - }, - { - "include-filter": "android.permission.cts.SplitPermissionTest" - }, - { - "include-filter": "android.permission.cts.PermissionFlagsTest" - }, - { - "include-filter": "android.permission.cts.SharedUidPermissionsTest" - } - ] + "name": "CtsPermissionTestCases_Platform" }, { "name": "CtsAppSecurityHostTestCases", diff --git a/services/core/java/com/android/server/policy/TEST_MAPPING b/services/core/java/com/android/server/policy/TEST_MAPPING index 338b479f1ad1..bdb174d98137 100644 --- a/services/core/java/com/android/server/policy/TEST_MAPPING +++ b/services/core/java/com/android/server/policy/TEST_MAPPING @@ -46,18 +46,7 @@ ] }, { - "name": "CtsPermissionTestCases", - "options": [ - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "include-filter": "android.permission.cts.SplitPermissionTest" - }, - { - "include-filter": "android.permission.cts.BackgroundPermissionsTest" - } - ] + "name": "CtsPermissionTestCases_Platform" }, { "name": "CtsBackupTestCases", diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java index d0b70c391579..da8b01ac86fb 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java @@ -176,8 +176,9 @@ public class KeyguardServiceDelegate { final DreamManagerInternal dreamManager = LocalServices.getService(DreamManagerInternal.class); - - dreamManager.registerDreamManagerStateListener(mDreamManagerStateListener); + if(dreamManager != null){ + dreamManager.registerDreamManagerStateListener(mDreamManagerStateListener); + } } private final ServiceConnection mKeyguardConnection = new ServiceConnection() { diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index a27360df798a..12e7fd010e3d 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1379,8 +1379,10 @@ public final class PowerManagerService extends SystemService new DisplayGroupPowerChangeListener(); mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener); - // This DreamManager method does not acquire a lock, so it should be safe to call. - mDreamManager.registerDreamManagerStateListener(new DreamManagerStateListener()); + if(mDreamManager != null){ + // This DreamManager method does not acquire a lock, so it should be safe to call. + mDreamManager.registerDreamManagerStateListener(new DreamManagerStateListener()); + } mWirelessChargerDetector = mInjector.createWirelessChargerDetector(sensorManager, mInjector.createSuspendBlocker( @@ -3543,7 +3545,7 @@ public final class PowerManagerService extends SystemService } // Stop dream. - if (isDreaming) { + if (isDreaming && mDreamManager != null) { mDreamManager.stopDream(/* immediate= */ false, "power manager request" /*reason*/); } } diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java index 9a4c60d7625e..68760aae8d9d 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverStateMachine.java @@ -864,7 +864,8 @@ public class BatterySaverStateMachine { buildNotification(DYNAMIC_MODE_NOTIF_CHANNEL_ID, R.string.dynamic_mode_notification_title, R.string.dynamic_mode_notification_summary, - Settings.ACTION_BATTERY_SAVER_SETTINGS, 0L), + Settings.ACTION_BATTERY_SAVER_SETTINGS, 0L, + R.drawable.ic_settings), UserHandle.ALL); }); } @@ -889,7 +890,8 @@ public class BatterySaverStateMachine { R.string.dynamic_mode_notification_summary_v2, Settings.ACTION_BATTERY_SAVER_SETTINGS, 0L /* timeoutMs */, - highlightBundle), + highlightBundle, + R.drawable.ic_qs_battery_saver), UserHandle.ALL); }); } @@ -911,7 +913,8 @@ public class BatterySaverStateMachine { R.string.battery_saver_off_notification_title, R.string.battery_saver_charged_notification_summary, Settings.ACTION_BATTERY_SAVER_SETTINGS, - STICKY_DISABLED_NOTIFY_TIMEOUT_MS), + STICKY_DISABLED_NOTIFY_TIMEOUT_MS, + R.drawable.ic_settings), UserHandle.ALL); }); } @@ -926,7 +929,7 @@ public class BatterySaverStateMachine { } private Notification buildNotification(@NonNull String channelId, @StringRes int titleId, - @StringRes int summaryId, @NonNull String intentAction, long timeoutMs) { + @StringRes int summaryId, @NonNull String intentAction, long timeoutMs, int iconResId) { Resources res = mContext.getResources(); Intent intent = new Intent(intentAction); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -937,7 +940,7 @@ public class BatterySaverStateMachine { final String summary = res.getString(summaryId); return new Notification.Builder(mContext, channelId) - .setSmallIcon(R.drawable.ic_battery) + .setSmallIcon(iconResId) .setContentTitle(title) .setContentText(summary) .setContentIntent(batterySaverIntent) @@ -950,7 +953,7 @@ public class BatterySaverStateMachine { private Notification buildNotificationV2(@NonNull String channelId, @StringRes int titleId, @StringRes int summaryId, @NonNull String intentAction, long timeoutMs, - @NonNull Bundle highlightBundle) { + @NonNull Bundle highlightBundle, int iconResId) { Resources res = mContext.getResources(); Intent intent = new Intent(intentAction) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -963,7 +966,7 @@ public class BatterySaverStateMachine { final String summary = res.getString(summaryId); return new Notification.Builder(mContext, channelId) - .setSmallIcon(R.drawable.ic_battery) + .setSmallIcon(iconResId) .setContentTitle(title) .setContentText(summary) .setContentIntent(batterySaverIntent) diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index b45651d7aafc..385561d8c1a8 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -15300,15 +15300,6 @@ public class BatteryStatsImpl extends BatteryStats { mHistory.writeHistoryItem(elapsedRealtimeMs, uptimeMs); } } - if (!onBattery && - (status == BatteryManager.BATTERY_STATUS_FULL || - status == BatteryManager.BATTERY_STATUS_UNKNOWN)) { - // We don't record history while we are plugged in and fully charged - // (or when battery is not present). The next time we are - // unplugged, history will be cleared. - mHistory.setHistoryRecordingEnabled(DEBUG); - } - mLastLearnedBatteryCapacityUah = chargeFullUah; if (mMinLearnedBatteryCapacityUah == -1) { mMinLearnedBatteryCapacityUah = chargeFullUah; diff --git a/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java b/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java index d6bf02fcdc47..6466519356b7 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java +++ b/services/core/java/com/android/server/statusbar/StatusBarShellCommand.java @@ -190,6 +190,9 @@ public class StatusBarShellCommand extends ShellCommand { case "notification-icons": info.setNotificationIconsDisabled(true); break; + case "quick-settings": + info.setQuickSettingsDisabled(true); + break; default: break; } @@ -277,6 +280,7 @@ public class StatusBarShellCommand extends ShellCommand { pw.println(" system-icons - disable system icons appearing in status bar"); pw.println(" clock - disable clock appearing in status bar"); pw.println(" notification-icons - disable notification icons from status bar"); + pw.println(" quick-settings - disable Quick Settings"); pw.println(""); pw.println(" tracing (start | stop)"); pw.println(" Start or stop SystemUI tracing"); diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index ba2594abd4d4..f53dda6ee35b 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -42,6 +42,7 @@ import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_INFO; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG; import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; import static com.android.server.wallpaper.WallpaperUtils.makeWallpaperIdLocked; +import static com.android.window.flags.Flags.avoidRebindingIntentionallyDisconnectedWallpaper; import static com.android.window.flags.Flags.multiCrop; import static com.android.window.flags.Flags.offloadColorExtraction; @@ -897,6 +898,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return; } + if (avoidRebindingIntentionallyDisconnectedWallpaper() + && mWallpaper.connection == null) { + Slog.w(TAG, "Trying to reset an intentionally disconnected wallpaper!"); + return; + } + if (!mWallpaper.wallpaperUpdating && mWallpaper.userId == mCurrentUserId) { Slog.w(TAG, "Wallpaper reconnect timed out for " + mWallpaper.wallpaperComponent + ", reverting to built-in wallpaper!"); @@ -1066,6 +1073,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (mWallpaper.wallpaperUpdating) { return; } + + if (avoidRebindingIntentionallyDisconnectedWallpaper() + && mWallpaper.connection == null) { + Slog.w(TAG, "Trying to rebind an intentionally disconnected wallpaper!"); + return; + } + final ComponentName wpService = mWallpaper.wallpaperComponent; // The broadcast of package update could be delayed after service disconnected. Try // to re-bind the service for 10 seconds. diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index e27b2beb3d5a..c1e859ddcccf 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -864,8 +864,9 @@ class ActivityClientController extends IActivityClientController.Stub { if (transition != null) { if (changed) { // Always set as scene transition because it expects to be a jump-cut. - transition.setOverrideAnimation(TransitionInfo.AnimationOptions - .makeSceneTransitionAnimOptions(), null, null); + transition.setOverrideAnimation( + TransitionInfo.AnimationOptions.makeSceneTransitionAnimOptions(), r, + null, null); r.mTransitionController.requestStartTransition(transition, null /*startTask */, null /* remoteTransition */, null /* displayChange */); @@ -910,8 +911,9 @@ class ActivityClientController extends IActivityClientController.Stub { && under.returningOptions.getAnimationType() == ANIM_SCENE_TRANSITION) { // Pass along the scene-transition animation-type - transition.setOverrideAnimation(TransitionInfo.AnimationOptions - .makeSceneTransitionAnimOptions(), null, null); + transition.setOverrideAnimation(TransitionInfo + .AnimationOptions.makeSceneTransitionAnimOptions(), r, + null, null); } } else { transition.abort(); @@ -1508,7 +1510,7 @@ class ActivityClientController extends IActivityClientController.Stub { r.mOverrideTaskTransition); r.mTransitionController.setOverrideAnimation( TransitionInfo.AnimationOptions.makeCustomAnimOptions(packageName, - enterAnim, exitAnim, backgroundColor, r.mOverrideTaskTransition), + enterAnim, exitAnim, backgroundColor, r.mOverrideTaskTransition), r, null /* startCallback */, null /* finishCallback */); } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 4b0409b53bbb..235a2115a964 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -353,6 +353,7 @@ import android.window.SplashScreen; import android.window.SplashScreenView; import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.TaskSnapshot; +import android.window.TransitionInfo; import android.window.TransitionInfo.AnimationOptions; import android.window.WindowContainerToken; import android.window.WindowOnBackInvokedDispatcher; @@ -5034,7 +5035,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // controller but don't clear the animation information from the options since they // need to be sent to the animating activity. mTransitionController.setOverrideAnimation( - AnimationOptions.makeSceneTransitionAnimOptions(), null, null); + TransitionInfo.AnimationOptions.makeSceneTransitionAnimOptions(), this, + null, null); return; } applyOptionsAnimation(mPendingOptions, intent); @@ -5157,7 +5159,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (options != null) { - mTransitionController.setOverrideAnimation(options, startCallback, finishCallback); + mTransitionController.setOverrideAnimation(options, this, startCallback, + finishCallback); } } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 64791112a129..bf18a438f9af 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1945,7 +1945,7 @@ class ActivityStarter { && sourceRecord != null && sourceRecord.getTask() == mStartActivity.getTask() && balVerdict.allows()) { mRootWindowContainer.moveActivityToPinnedRootTask(mStartActivity, - sourceRecord, "launch-into-pip"); + sourceRecord, "launch-into-pip", null /* bounds */); } mSupervisor.getBackgroundActivityLaunchController() diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index fc087e3d62a3..f5476f29849a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3796,9 +3796,22 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { r.shortComponentName, Boolean.toString(isAutoEnter)); r.setPictureInPictureParams(params); r.mAutoEnteringPip = isAutoEnter; - mRootWindowContainer.moveActivityToPinnedRootTask(r, - null /* launchIntoPipHostActivity */, "enterPictureInPictureMode", - transition); + + if (transition != null) { + mRootWindowContainer.moveActivityToPinnedRootTaskAndRequestStart(r, + "enterPictureInPictureMode"); + } else if (getTransitionController().isCollecting() + || !getTransitionController().isShellTransitionsEnabled()) { + mRootWindowContainer.moveActivityToPinnedRootTask(r, + null /* launchIntoPipHostActivity */, "enterPictureInPictureMode", + null /* bounds */); + } else { + // Need to make a transition just for this. This shouldn't really happen + // though because if transition == null, we should be part of an existing one. + getTransitionController().createTransition(TRANSIT_PIP); + mRootWindowContainer.moveActivityToPinnedRootTaskAndRequestStart(r, + "enterPictureInPictureMode"); + } // Continue the pausing process after entering pip. if (r.isState(PAUSING) && r.mPauseSchedulePendingForPip) { r.getTask().schedulePauseActivity(r, false /* userLeaving */, diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index c44f838b56c1..3710f7fcb7cd 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -574,10 +574,15 @@ class BackNavigationController { private void scheduleAnimation(@NonNull AnimationHandler.ScheduleAnimationBuilder builder) { mPendingAnimation = builder.build(); - mWindowManagerService.mWindowPlacerLocked.requestTraversal(); - if (mShowWallpaper) { - mWindowManagerService.getDefaultDisplayContentLocked().mWallpaperController - .adjustWallpaperWindows(); + if (mAnimationHandler.mOpenAnimAdaptor != null + && mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition != null) { + startAnimation(); + } else { + mWindowManagerService.mWindowPlacerLocked.requestTraversal(); + if (mShowWallpaper) { + mWindowManagerService.getDefaultDisplayContentLocked().mWallpaperController + .adjustWallpaperWindows(); + } } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index a6d965955272..866dcd56ea91 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2032,33 +2032,39 @@ class RootWindowContainer extends WindowContainer<DisplayContent> onTop); } - void moveActivityToPinnedRootTask(@NonNull ActivityRecord r, - @Nullable ActivityRecord launchIntoPipHostActivity, String reason) { - moveActivityToPinnedRootTask(r, launchIntoPipHostActivity, reason, null /* transition */); + /** Wrapper/Helper for tests */ + void moveActivityToPinnedRootTask(@NonNull ActivityRecord r, String reason) { + Transition newTransit = (r.mTransitionController.isCollecting() + || !r.mTransitionController.isShellTransitionsEnabled()) + ? null : r.mTransitionController.createTransition(TRANSIT_PIP); + moveActivityToPinnedRootTaskInner(r, null /* launchIntoPipHostActivity */, reason, + null /* bounds */, newTransit != null); } void moveActivityToPinnedRootTask(@NonNull ActivityRecord r, @Nullable ActivityRecord launchIntoPipHostActivity, String reason, - @Nullable Transition transition) { - moveActivityToPinnedRootTask(r, launchIntoPipHostActivity, reason, transition, - null /* bounds */); + @Nullable Rect bounds) { + moveActivityToPinnedRootTaskInner(r, launchIntoPipHostActivity, reason, bounds, + false /* requestStart */); } - void moveActivityToPinnedRootTask(@NonNull ActivityRecord r, + /** + * Moves activity to pinned in the provided transition and also requests start on that + * Transition at an appropriate time. + */ + void moveActivityToPinnedRootTaskAndRequestStart(@NonNull ActivityRecord r, String reason) { + moveActivityToPinnedRootTaskInner(r, null /* launchIntoPipHostActivity */, reason, + null /* bounds */, true /* requestStart */); + } + + private void moveActivityToPinnedRootTaskInner(@NonNull ActivityRecord r, @Nullable ActivityRecord launchIntoPipHostActivity, String reason, - @Nullable Transition transition, @Nullable Rect bounds) { + @Nullable Rect bounds, boolean requestStart) { final TaskDisplayArea taskDisplayArea = r.getDisplayArea(); final Task task = r.getTask(); final Task rootTask; - Transition newTransition = transition; - // Create a transition now (if not provided) to collect the current pinned Task dismiss. - // Only do the create here as the Task (trigger) to enter PIP is not ready yet. final TransitionController transitionController = task.mTransitionController; - if (newTransition == null && !transitionController.isCollecting() - && transitionController.getTransitionPlayer() != null) { - newTransition = transitionController.createTransition(TRANSIT_PIP); - } transitionController.deferTransitionReady(); Transition.ReadyCondition pipChangesApplied = new Transition.ReadyCondition("movedToPip"); @@ -2273,14 +2279,16 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } } - if (newTransition != null) { + // can be null (for now) if shell transitions are disabled or inactive at this time + final Transition transit = transitionController.getCollectingTransition(); + if (requestStart && transit != null) { // Request at end since we want task-organizer events from ensureActivitiesVisible // to be recognized. - transitionController.requestStartTransition(newTransition, rootTask, + transitionController.requestStartTransition(transit, rootTask, null /* remoteTransition */, null /* displayChange */); // A new transition was created just for this operations. Since the operation is // complete, mark it as ready. - newTransition.setReady(rootTask, true /* ready */); + transit.setReady(rootTask, true /* ready */); } resumeFocusedTasksTopActivities(); diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java index 0f9c001dffa8..52994c70f183 100644 --- a/services/core/java/com/android/server/wm/SnapshotController.java +++ b/services/core/java/com/android/server/wm/SnapshotController.java @@ -31,6 +31,8 @@ import android.util.ArrayMap; import android.view.WindowManager; import android.window.TaskSnapshot; +import com.android.window.flags.Flags; + import java.io.PrintWriter; import java.util.ArrayList; @@ -150,6 +152,9 @@ class SnapshotController { if (mOpenActivities.isEmpty()) { return false; } + if (Flags.alwaysCaptureActivitySnapshot()) { + return true; + } for (int i = mOpenActivities.size() - 1; i >= 0; --i) { if (!mOpenActivities.get(i).mOptInOnBackInvoked) { return false; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 82ede7efda5a..6bfa32a97ddb 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -953,10 +953,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { * Set animation options for collecting transition by ActivityRecord. * @param options AnimationOptions captured from ActivityOptions */ - void setOverrideAnimation(@Nullable AnimationOptions options, + void setOverrideAnimation(@Nullable AnimationOptions options, @NonNull ActivityRecord r, @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) { if (!isCollecting()) return; mOverrideOptions = options; + if (mOverrideOptions != null) { + mOverrideOptions.setUserId(r.mUserId); + } sendRemoteCallback(mClientAnimationStartCallback); mClientAnimationStartCallback = startCallback; mClientAnimationFinishCallback = finishCallback; @@ -2818,7 +2821,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } final SurfaceControl rootLeash = leashReference.makeAnimationLeash().setName( - "Transition Root: " + leashReference.getName()) + "Transition Root: " + leashReference.getName()) .setCallsite("Transition.calculateTransitionRoots").build(); rootLeash.setUnreleasedWarningCallSite("Transition.calculateTransitionRoots"); // Update layers to start transaction because we prevent assignment during collect, so @@ -2989,7 +2992,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Create the options based on this change's custom animations and layout // parameters animOptions = getOptions(activityRecord /* customAnimActivity */, - activityRecord /* animLpActivity */); + activityRecord /* animLpActivity */); + animOptions.setUserId(activityRecord.mUserId); if (!change.hasFlags(FLAG_TRANSLUCENT)) { // If this change is not translucent, its options are going to be // inherited by the changes below @@ -2999,6 +3003,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } else if (activityRecord != null && animOptionsForActivityTransition != null) { // Use the same options from the top activity for all the activities animOptions = animOptionsForActivityTransition; + animOptions.setUserId(activityRecord.mUserId); } else if (Flags.activityEmbeddingOverlayPresentationFlag() && isEmbeddedTaskFragment) { final TaskFragmentAnimationParams params = taskFragment.getAnimationParams(); @@ -3011,6 +3016,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { params.getOpenAnimationResId(), params.getChangeAnimationResId(), params.getCloseAnimationResId(), 0 /* backgroundColor */, false /* overrideTaskTransition */); + animOptions.setUserId(taskFragment.getTask().mUserId); } } if (animOptions != null) { @@ -3074,6 +3080,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { animOptions); animOptions = addCustomActivityTransition(customAnimActivity, false /* open */, animOptions); + if (animOptions != null) { + animOptions.setUserId(customAnimActivity.mUserId); + } } // Layout parameters @@ -3087,10 +3096,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // are running an app starting animation, in which case we don't want the app to be // able to change its animation directly. if (animOptions != null) { + animOptions.setUserId(animLpActivity.mUserId); animOptions.addOptionsFromLayoutParameters(animLp); } else { animOptions = TransitionInfo.AnimationOptions .makeAnimOptionsFromLayoutParameters(animLp); + animOptions.setUserId(animLpActivity.mUserId); } } return animOptions; @@ -3116,6 +3127,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (animOptions == null) { animOptions = TransitionInfo.AnimationOptions .makeCommonAnimOptions(activity.packageName); + animOptions.setUserId(activity.mUserId); } animOptions.addCustomActivityTransition(open, customAnim.mEnterAnim, customAnim.mExitAnim, customAnim.mBackgroundColor); @@ -3244,7 +3256,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Remote animations always win, but fullscreen windows override non-fullscreen windows. ActivityRecord result = lookForTopWindowWithFilter(sortedTargets, w -> w.getRemoteAnimationDefinition() != null - && w.getRemoteAnimationDefinition().hasTransition(transit, activityTypes)); + && w.getRemoteAnimationDefinition().hasTransition(transit, activityTypes)); if (result != null) { return result; } @@ -3291,7 +3303,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { private void validateKeyguardOcclusion() { if ((mFlags & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0) { mController.mStateValidators.add( - mController.mAtm.mWindowManager.mPolicy::applyKeyguardOcclusionChange); + mController.mAtm.mWindowManager.mPolicy::applyKeyguardOcclusionChange); } } @@ -3900,9 +3912,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { /** @return true if all tracked subtrees are ready. */ boolean allReady() { - ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " allReady query: used=%b " - + "override=%b defer=%d states=[%s]", mUsed, mReadyOverride, mDeferReadyDepth, - groupsToString()); + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " allReady query: used=%b " + "override=%b defer=%d states=[%s]", mUsed, + mReadyOverride, mDeferReadyDepth, groupsToString()); // If the readiness has never been touched, mUsed will be false. We never want to // consider a transition ready if nothing has been reported on it. if (!mUsed) return false; diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 56a24dd4ca49..1d2a605e8dae 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -880,10 +880,10 @@ class TransitionController { } /** @see Transition#setOverrideAnimation */ - void setOverrideAnimation(TransitionInfo.AnimationOptions options, + void setOverrideAnimation(TransitionInfo.AnimationOptions options, ActivityRecord r, @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) { if (mCollectingTransition == null) return; - mCollectingTransition.setOverrideAnimation(options, startCallback, finishCallback); + mCollectingTransition.setOverrideAnimation(options, r, startCallback, finishCallback); } void setNoAnimation(WindowContainer wc) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 60ccdc72ee09..476443aa2050 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1339,7 +1339,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub Rect entryBounds = hop.getBounds(); mService.mRootWindowContainer.moveActivityToPinnedRootTask( pipActivity, null /* launchIntoPipHostActivity */, - "moveActivityToPinnedRootTask", null /* transition */, entryBounds); + "moveActivityToPinnedRootTask", entryBounds); if (pipActivity.isState(PAUSING) && pipActivity.mPauseSchedulePendingForPip) { // Continue the pausing process. This must be done after moving PiP activity to diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index 6a0dd5a04f82..5eec0124a9e3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -1325,27 +1325,7 @@ class ActiveAdmin { pw.print("encryptionRequested="); pw.println(encryptionRequested); - if (!Flags.dumpsysPolicyEngineMigrationEnabled()) { - pw.print("disableCamera="); - pw.println(disableCamera); - - pw.print("disableScreenCapture="); - pw.println(disableScreenCapture); - - pw.print("requireAutoTime="); - pw.println(requireAutoTime); - - if (permittedInputMethods != null) { - pw.print("permittedInputMethods="); - pw.println(permittedInputMethods); - } - - pw.println("userRestrictions:"); - UserRestrictionsUtils.dumpRestrictions(pw, " ", userRestrictions); - } - - if (!Flags.policyEngineMigrationV2Enabled() - || !Flags.dumpsysPolicyEngineMigrationEnabled()) { + if (!Flags.policyEngineMigrationV2Enabled()) { pw.print("mUsbDataSignaling="); pw.println(mUsbDataSignalingEnabled); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index a80ee0f66742..4abbdee65acb 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -11479,10 +11479,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { pw.println(); mStatLogger.dump(pw); pw.println(); - if (Flags.dumpsysPolicyEngineMigrationEnabled()) { - mDevicePolicyEngine.dump(pw); - pw.println(); - } + mDevicePolicyEngine.dump(pw); + pw.println(); pw.println("Encryption Status: " + getEncryptionStatusName(getEncryptionStatus())); pw.println("Logout user: " + getLogoutUserIdUnchecked()); pw.println(); @@ -12682,14 +12680,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)); checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_CREATE_AND_MANAGE_USER); - if (Flags.headlessDeviceOwnerSingleUserEnabled()) { - // Block this method if the device is in headless main user mode - Preconditions.checkCallAuthorization( - !mInjector.userManagerIsHeadlessSystemUserMode() - || getHeadlessDeviceOwnerModeForDeviceOwner() - != HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER, - "createAndManageUser was called while in headless single user mode"); - } + // Block this method if the device is in headless main user mode + Preconditions.checkCallAuthorization( + !mInjector.userManagerIsHeadlessSystemUserMode() + || getHeadlessDeviceOwnerModeForDeviceOwner() + != HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER, + "createAndManageUser was called while in headless single user mode"); // Only allow the system user to use this method Preconditions.checkCallAuthorization(caller.getUserHandle().isSystem(), @@ -13976,11 +13972,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { UserManager.DISALLOW_THREAD_NETWORK, new String[]{MANAGE_DEVICE_POLICY_THREAD_NETWORK}); } - if (Flags.assistContentUserRestrictionEnabled()) { - USER_RESTRICTION_PERMISSIONS.put( - UserManager.DISALLOW_ASSIST_CONTENT, - new String[]{MANAGE_DEVICE_POLICY_ASSIST_CONTENT}); - } + USER_RESTRICTION_PERMISSIONS.put( + UserManager.DISALLOW_ASSIST_CONTENT, + new String[]{MANAGE_DEVICE_POLICY_ASSIST_CONTENT}); USER_RESTRICTION_PERMISSIONS.put( UserManager.DISALLOW_ULTRA_WIDEBAND_RADIO, new String[]{MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION}); USER_RESTRICTION_PERMISSIONS.put( @@ -17315,7 +17309,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED; } - if (Flags.headlessDeviceOwnerSingleUserEnabled() && isHeadlessModeSingleUser) { + if (isHeadlessModeSingleUser) { ensureSetUpUser = mUserManagerInternal.getMainUserId(); if (ensureSetUpUser == UserHandle.USER_NULL) { return STATUS_HEADLESS_ONLY_SYSTEM_USER; @@ -19759,16 +19753,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } private void transferSubscriptionOwnership(ComponentName admin, ComponentName target) { - if (Flags.esimManagementEnabled()) { - SubscriptionManager subscriptionManager = mContext.getSystemService( - SubscriptionManager.class); - for (int subId : getSubscriptionIdsInternal(admin.getPackageName()).toArray()) { - try { - subscriptionManager.setGroupOwner(subId, target.getPackageName()); - } catch (Exception e) { - // Shouldn't happen. - Slogf.e(LOG_TAG, e, "Error setting group owner for subId: " + subId); - } + SubscriptionManager subscriptionManager = mContext.getSystemService( + SubscriptionManager.class); + for (int subId : getSubscriptionIdsInternal(admin.getPackageName()).toArray()) { + try { + subscriptionManager.setGroupOwner(subId, target.getPackageName()); + } catch (Exception e) { + // Shouldn't happen. + Slogf.e(LOG_TAG, e, "Error setting group owner for subId: " + subId); } } } @@ -20666,9 +20658,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // have OP_RUN_ANY_IN_BACKGROUND app op and won't execute in the background. The // code below grants that app op, and once the exemption is in place, the user // won't be able to disable background usage anymore. - if (Flags.powerExemptionBgUsageFix() - && exemption == EXEMPT_FROM_POWER_RESTRICTIONS - && newMode == MODE_ALLOWED) { + if (exemption == EXEMPT_FROM_POWER_RESTRICTIONS && newMode == MODE_ALLOWED) { setBgUsageAppOp(appOpsMgr, appInfo); } } @@ -22065,8 +22055,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { setTimeAndTimezone(provisioningParams.getTimeZone(), provisioningParams.getLocalTime()); setLocale(provisioningParams.getLocale()); - int deviceOwnerUserId = Flags.headlessDeviceOwnerSingleUserEnabled() - && isSingleUserMode && mInjector.userManagerIsHeadlessSystemUserMode() + int deviceOwnerUserId = + isSingleUserMode && mInjector.userManagerIsHeadlessSystemUserMode() ? mUserManagerInternal.getMainUserId() : UserHandle.USER_SYSTEM; if (!removeNonRequiredAppsForManagedDevice( diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 19a942cd2eed..24ee46fbcd6f 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -536,7 +536,6 @@ final class PolicyDefinition<V> { USER_RESTRICTION_FLAGS.put( UserManager.DISALLOW_THREAD_NETWORK, POLICY_FLAG_GLOBAL_ONLY_POLICY); } - USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_ASSIST_CONTENT, /* flags= */ 0); for (String key : USER_RESTRICTION_FLAGS.keySet()) { createAndAddUserRestrictionPolicyDefinition(key, USER_RESTRICTION_FLAGS.get(key)); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java index e1cb37dbeef5..8068d46d6a9d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java @@ -238,9 +238,7 @@ final class PolicyEnforcerCallbacks { } for (int user : resolveUsers(userId)) { - if (Flags.disallowUserControlBgUsageFix()) { - setBgUsageAppOp(packages, pmi, user, appOpsManager); - } + setBgUsageAppOp(packages, pmi, user, appOpsManager); if (Flags.disallowUserControlStoppedStateFix()) { for (String packageName : packages) { pmi.setPackageStoppedState(packageName, false, user); diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamOverlayServiceTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamOverlayServiceTest.java index 54f46078d30b..1abc557c8cce 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamOverlayServiceTest.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamOverlayServiceTest.java @@ -18,7 +18,9 @@ package com.android.server.dreams; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -27,7 +29,9 @@ import android.content.ComponentName; import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; import android.service.dreams.DreamOverlayService; +import android.service.dreams.Flags; import android.service.dreams.IDreamOverlay; import android.service.dreams.IDreamOverlayCallback; import android.service.dreams.IDreamOverlayClient; @@ -136,7 +140,7 @@ public class DreamOverlayServiceTest { // Start the dream. client.startDream(mLayoutParams, mOverlayCallback, - FIRST_DREAM_COMPONENT.flattenToString(), false); + FIRST_DREAM_COMPONENT.flattenToString(), false, false); // The callback should not have run yet. verify(monitor, never()).onStartDream(); @@ -194,22 +198,24 @@ public class DreamOverlayServiceTest { // Start a dream with the first client and ensure the dream is now active from the // overlay's perspective. firstClient.startDream(mLayoutParams, mOverlayCallback, - FIRST_DREAM_COMPONENT.flattenToString(), false); + FIRST_DREAM_COMPONENT.flattenToString(), true, false); verify(monitor).onStartDream(); assertThat(service.getDreamComponent()).isEqualTo(FIRST_DREAM_COMPONENT); + assertThat(service.isDreamInPreviewMode()).isTrue(); Mockito.clearInvocations(monitor); // Start a dream from the second client and verify that the overlay has both cycled to // the new dream (ended/started). secondClient.startDream(mLayoutParams, mOverlayCallback, - SECOND_DREAM_COMPONENT.flattenToString(), false); + SECOND_DREAM_COMPONENT.flattenToString(), false, false); verify(monitor).onEndDream(); verify(monitor).onStartDream(); assertThat(service.getDreamComponent()).isEqualTo(SECOND_DREAM_COMPONENT); + assertThat(service.isDreamInPreviewMode()).isFalse(); Mockito.clearInvocations(monitor); @@ -221,6 +227,47 @@ public class DreamOverlayServiceTest { verify(monitor, never()).onWakeUp(); } + /** + * Verifies that only the currently started dream is able to affect the overlay. + */ + @Test + @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT) + public void testRedirectToWakeAcrossClients() throws RemoteException { + doAnswer(invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }).when(mExecutor).execute(any()); + + final TestDreamOverlayService.Monitor monitor = Mockito.mock( + TestDreamOverlayService.Monitor.class); + final TestDreamOverlayService service = new TestDreamOverlayService(monitor, mExecutor); + final IBinder binder = service.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(binder); + + service.redirectWake(true); + + final IDreamOverlayClient client = getClient(overlay); + + // Start the dream. + client.startDream(mLayoutParams, mOverlayCallback, + FIRST_DREAM_COMPONENT.flattenToString(), false, false); + // Make sure redirect state is set on dream. + verify(mOverlayCallback).onRedirectWake(eq(true)); + + // Make sure new changes are propagated. + clearInvocations(mOverlayCallback); + service.redirectWake(false); + verify(mOverlayCallback).onRedirectWake(eq(false)); + + + // Start another dream, make sure new dream is informed of current state. + service.redirectWake(true); + clearInvocations(mOverlayCallback); + client.startDream(mLayoutParams, mOverlayCallback, + FIRST_DREAM_COMPONENT.flattenToString(), false, false); + verify(mOverlayCallback).onRedirectWake(eq(true)); + } + private static IDreamOverlayClient getClient(IDreamOverlay overlay) throws RemoteException { final OverlayClientCallback callback = new OverlayClientCallback(); overlay.getClient(callback); diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java index 43aa7fe46ca5..7c239ef02e58 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java @@ -385,7 +385,8 @@ public class TestDreamEnvironment { final ArgumentCaptor<IDreamOverlayCallback> overlayCallbackCaptor = ArgumentCaptor.forClass(IDreamOverlayCallback.class); verify(mDreamOverlayClient, description("dream client not informed of dream start")) - .startDream(any(), overlayCallbackCaptor.capture(), any(), anyBoolean()); + .startDream(any(), overlayCallbackCaptor.capture(), any(), anyBoolean(), + anyBoolean()); mDreamOverlayCallback = overlayCallbackCaptor.getValue(); } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 8656b991b5fc..51aa5284f5f2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -46,6 +46,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.server.am.ActivityManagerService.FOLLOW_UP_OOMADJUSTER_UPDATE_MSG; import static com.android.server.am.ProcessList.BACKUP_APP_ADJ; +import static com.android.server.am.ProcessList.CACHED_APP_IMPORTANCE_LEVELS; import static com.android.server.am.ProcessList.CACHED_APP_MAX_ADJ; import static com.android.server.am.ProcessList.CACHED_APP_MIN_ADJ; import static com.android.server.am.ProcessList.FOREGROUND_APP_ADJ; @@ -844,6 +845,49 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test + public void testUpdateOomAdj_DoAll_PreviousApp() { + final int numberOfApps = 15; + final ProcessRecord[] apps = new ProcessRecord[numberOfApps]; + for (int i = 0; i < numberOfApps; i++) { + apps[i] = spy(makeDefaultProcessRecord(MOCKAPP_PID + i, MOCKAPP_UID + i, + MOCKAPP_PROCESSNAME + i, MOCKAPP_PACKAGENAME + i, true)); + final WindowProcessController wpc = apps[i].getWindowProcessController(); + doReturn(true).when(wpc).isPreviousProcess(); + doReturn(true).when(wpc).hasActivities(); + } + mService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE); + setProcessesToLru(apps); + mService.mOomAdjuster.updateOomAdjLocked(OOM_ADJ_REASON_NONE); + + for (int i = 0; i < numberOfApps; i++) { + assertProcStates(apps[i], PROCESS_STATE_LAST_ACTIVITY, PREVIOUS_APP_ADJ, + SCHED_GROUP_BACKGROUND, "previous"); + } + + if (!Flags.followUpOomadjUpdates()) return; + + for (int i = 0; i < numberOfApps; i++) { + final ArgumentCaptor<Long> followUpTimeCaptor = ArgumentCaptor.forClass(Long.class); + verify(mService.mHandler).sendEmptyMessageAtTime(eq(FOLLOW_UP_OOMADJUSTER_UPDATE_MSG), + followUpTimeCaptor.capture()); + mInjector.jumpUptimeAheadTo(followUpTimeCaptor.getValue()); + } + + mService.mOomAdjuster.updateOomAdjFollowUpTargetsLocked(); + + for (int i = 0; i < numberOfApps; i++) { + final int mruIndex = numberOfApps - i - 1; + int expectedAdj = CACHED_APP_MIN_ADJ + (mruIndex * 2 * CACHED_APP_IMPORTANCE_LEVELS); + if (expectedAdj > CACHED_APP_MAX_ADJ) { + expectedAdj = CACHED_APP_MAX_ADJ; + } + assertProcStates(apps[i], PROCESS_STATE_LAST_ACTIVITY, expectedAdj, + SCHED_GROUP_BACKGROUND, "previous-expired"); + } + } + + @SuppressWarnings("GuardedBy") + @Test public void testUpdateOomAdj_DoOne_Backup() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index bc2fd73f22ae..2f7b8d26bdd9 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -200,7 +200,8 @@ public class AuthSessionTest { eq(TEST_REQUEST_ID), eq(sensor.getCookie()), anyBoolean() /* allowBackgroundAuthentication */, - anyBoolean() /* isForLegacyFingerprintManager */); + anyBoolean() /* isForLegacyFingerprintManager */, + eq(false) /* isMandatoryBiometrics */); } final int cookie1 = session.mPreAuthInfo.eligibleSensors.get(0).getCookie(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index d2961bc8a90f..6b8e414255cd 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -636,7 +636,8 @@ public class BiometricServiceTest { eq(TEST_REQUEST_ID), cookieCaptor.capture() /* cookie */, anyBoolean() /* allowBackgroundAuthentication */, - anyBoolean() /* isForLegacyFingerprintManager */); + anyBoolean() /* isForLegacyFingerprintManager */, + eq(false) /* isMandatoryBiometrics */); // onReadyForAuthentication, mAuthSession state OK mBiometricService.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookieCaptor.getValue()); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java index 4604b310edf7..613cb2019a46 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java @@ -98,7 +98,8 @@ public class AcquisitionClientTest { @NonNull ClientMonitorCallbackConverter callback) { super(context, lazyDaemon, token, callback, 0 /* userId */, "Test", 0 /* cookie */, TEST_SENSOR_ID /* sensorId */, true /* shouldVibrate */, - mock(BiometricLogger.class), mock(BiometricContext.class)); + mock(BiometricLogger.class), mock(BiometricContext.class), + false /* isMandatoryBiometrics */); } @Override diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java index ffc78110d496..4f07380dfb5a 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java @@ -62,7 +62,8 @@ public class BiometricSchedulerOperationTest { extends HalClientMonitor<T> { public InterruptableMonitor() { super(null, null, null, null, 0, null, 0, 0, - mock(BiometricLogger.class), mock(BiometricContext.class)); + mock(BiometricLogger.class), mock(BiometricContext.class), + false /* isMandatoryBiometrics */); } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index 36a7b3dff28d..90c07d489549 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -1051,6 +1051,11 @@ public class BiometricSchedulerTest { public String getAttributionTag() { return null; } + + @Override + public boolean isMandatoryBiometrics() { + return false; + } } private static class TestAuthenticationClient @@ -1176,7 +1181,7 @@ public class BiometricSchedulerTest { @NonNull Supplier<Object> lazyDaemon, int cookie, int protoEnum) { super(context, lazyDaemon, token /* token */, null /* listener */, 0 /* userId */, TAG, cookie, TEST_SENSOR_ID, mock(BiometricLogger.class), - mock(BiometricContext.class)); + mock(BiometricContext.class), false /* isMandatoryBiometrics */); mProtoEnum = protoEnum; } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java index 17b499e112bc..d6f7e21a2069 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java @@ -625,25 +625,10 @@ public class RebootEscrowManagerTests { // pretend reboot happens here when(mInjected.getBootCount()).thenReturn(1); - ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class); - ArgumentCaptor<Integer> metricsErrorCodeCaptor = ArgumentCaptor.forClass(Integer.class); - doNothing() - .when(mInjected) - .reportMetric( - metricsSuccessCaptor.capture(), - metricsErrorCodeCaptor.capture(), - eq(2) /* Server based */, - eq(1) /* attempt count */, - anyInt(), - eq(0) /* vbmeta status */, - anyInt()); + mService.loadRebootEscrowDataIfAvailable(null); verify(mServiceConnection, never()).unwrap(any(), anyLong()); verify(mCallbacks, never()).onRebootEscrowRestored(anyByte(), any(), anyInt()); - assertFalse(metricsSuccessCaptor.getValue()); - assertEquals( - Integer.valueOf(RebootEscrowManager.ERROR_NO_REBOOT_ESCROW_DATA), - metricsErrorCodeCaptor.getValue()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java b/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java index 55c48e07162b..f0a5f7583661 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserRestrictionsUtilsTest.java @@ -36,7 +36,6 @@ import android.util.SparseArray; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -55,11 +54,6 @@ public class UserRestrictionsUtilsTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Before - public void setUp() { - mSetFlagsRule.enableFlags(android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED); - } - @Test public void testNonNull() { Bundle out = UserRestrictionsUtils.nonNull(null); @@ -144,7 +138,6 @@ public class UserRestrictionsUtilsTest { @Test public void testCanProfileOwnerChange_restrictionRequiresOrgOwnedDevice_orgOwned() { - mSetFlagsRule.enableFlags(android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED); assertTrue(UserRestrictionsUtils.canProfileOwnerChange( UserManager.DISALLOW_SIM_GLOBALLY, false, @@ -157,7 +150,6 @@ public class UserRestrictionsUtilsTest { @Test public void testCanProfileOwnerChange_restrictionRequiresOrgOwnedDevice_notOrgOwned() { - mSetFlagsRule.enableFlags(android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED); assertFalse(UserRestrictionsUtils.canProfileOwnerChange( UserManager.DISALLOW_SIM_GLOBALLY, false, @@ -169,22 +161,7 @@ public class UserRestrictionsUtilsTest { } @Test - public void - testCanProfileOwnerChange_disabled_restrictionRequiresOrgOwnedDevice_notOrgOwned() { - mSetFlagsRule.disableFlags(android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED); - assertTrue(UserRestrictionsUtils.canProfileOwnerChange( - UserManager.DISALLOW_SIM_GLOBALLY, - false, - false)); - assertTrue(UserRestrictionsUtils.canProfileOwnerChange( - UserManager.DISALLOW_SIM_GLOBALLY, - true, - false)); - } - - @Test public void testCanProfileOwnerChange_restrictionNotRequiresOrgOwnedDevice_orgOwned() { - mSetFlagsRule.enableFlags(android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED); assertTrue(UserRestrictionsUtils.canProfileOwnerChange( UserManager.DISALLOW_ADJUST_VOLUME, false, @@ -197,7 +174,6 @@ public class UserRestrictionsUtilsTest { @Test public void testCanProfileOwnerChange_restrictionNotRequiresOrgOwnedDevice_notOrgOwned() { - mSetFlagsRule.enableFlags(android.app.admin.flags.Flags.FLAG_ESIM_MANAGEMENT_ENABLED); assertTrue(UserRestrictionsUtils.canProfileOwnerChange( UserManager.DISALLOW_ADJUST_VOLUME, false, diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java index f1db7130c1b9..957b5e04fef6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java @@ -371,8 +371,7 @@ public class RootWindowContainerTests extends WindowTestsBase { ensureTaskPlacement(fullscreenTask, firstActivity, secondActivity); // Move first activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(firstActivity, - null /* launchIntoPipHostActivity */, "initialMove"); + mRootWindowContainer.moveActivityToPinnedRootTask(firstActivity, "initialMove"); final TaskDisplayArea taskDisplayArea = fullscreenTask.getDisplayArea(); Task pinnedRootTask = taskDisplayArea.getRootPinnedTask(); @@ -381,8 +380,7 @@ public class RootWindowContainerTests extends WindowTestsBase { ensureTaskPlacement(fullscreenTask, secondActivity); // Move second activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(secondActivity, - null /* launchIntoPipHostActivity */, "secondMove"); + mRootWindowContainer.moveActivityToPinnedRootTask(secondActivity, "secondMove"); // Need to get root tasks again as a new instance might have been created. pinnedRootTask = taskDisplayArea.getRootPinnedTask(); @@ -413,8 +411,7 @@ public class RootWindowContainerTests extends WindowTestsBase { // Move first activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(secondActivity, - null /* launchIntoPipHostActivity */, "initialMove"); + mRootWindowContainer.moveActivityToPinnedRootTask(secondActivity, "initialMove"); assertTrue(firstActivity.mRequestForceTransition); } @@ -434,8 +431,7 @@ public class RootWindowContainerTests extends WindowTestsBase { transientActivity.setState(RESUMED, "test"); transientActivity.getTask().moveToFront("test"); - mRootWindowContainer.moveActivityToPinnedRootTask(activity2, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(activity2, "test"); assertEquals("Created PiP task must not change focus", transientActivity.getTask(), mRootWindowContainer.getTopDisplayFocusedRootTask()); final Task newPipTask = activity2.getTask(); @@ -460,8 +456,7 @@ public class RootWindowContainerTests extends WindowTestsBase { final Task task = activity.getTask(); // Move activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(activity, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(activity, "test"); // Ensure a task has moved over. ensureTaskPlacement(task, activity); @@ -499,8 +494,7 @@ public class RootWindowContainerTests extends WindowTestsBase { final Task task = activity.getTask(); // Move activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(activity, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(activity, "test"); // Ensure a task has moved over. ensureTaskPlacement(task, activity); @@ -524,8 +518,7 @@ public class RootWindowContainerTests extends WindowTestsBase { final ActivityRecord secondActivity = taskFragment.getBottomMostActivity(); // Move first activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(firstActivity, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(firstActivity, "test"); final TaskDisplayArea taskDisplayArea = fullscreenTask.getDisplayArea(); final Task pinnedRootTask = taskDisplayArea.getRootPinnedTask(); @@ -556,8 +549,7 @@ public class RootWindowContainerTests extends WindowTestsBase { final ActivityRecord topActivity = taskFragment.getTopMostActivity(); // Move the top activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(topActivity, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(topActivity, "test"); final Task pinnedRootTask = task.getDisplayArea().getRootPinnedTask(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 6be1af2c143f..cc1805aa933c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -368,8 +368,7 @@ public class TaskFragmentTest extends WindowTestsBase { assertNotEquals(TaskFragmentAnimationParams.DEFAULT, taskFragment0.getAnimationParams()); // Move activity to pinned root task. - mRootWindowContainer.moveActivityToPinnedRootTask(activity, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(activity, "test"); // Ensure taskFragment requested config is reset. assertEquals(taskFragment0, activity.getOrganizedTaskFragment()); @@ -399,8 +398,7 @@ public class TaskFragmentTest extends WindowTestsBase { spyOn(mAtm.mTaskFragmentOrganizerController); // Move activity to pinned. - mRootWindowContainer.moveActivityToPinnedRootTask(activity0, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(activity0, "test"); // Ensure taskFragment requested config is reset. assertTrue(taskFragment0.mClearedTaskFragmentForPip); @@ -434,8 +432,7 @@ public class TaskFragmentTest extends WindowTestsBase { .createActivityCount(1) .build(); final ActivityRecord activity = taskFragment.getTopMostActivity(); - mRootWindowContainer.moveActivityToPinnedRootTask(activity, - null /* launchIntoPipHostActivity */, "test"); + mRootWindowContainer.moveActivityToPinnedRootTask(activity, "test"); spyOn(mAtm.mTaskFragmentOrganizerController); assertEquals(mIOrganizer, activity.mLastTaskFragmentOrganizerBeforePip); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 55f74e9de192..45082d280587 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -230,8 +230,7 @@ public class TaskTests extends WindowTestsBase { final Task originalTask = activityMain.getTask(); final ActivityRecord activityPip = new ActivityBuilder(mAtm).setTask(originalTask).build(); activityPip.setState(RESUMED, "test"); - mAtm.mRootWindowContainer.moveActivityToPinnedRootTask(activityPip, - null /* launchIntoPipHostActivity */, "test"); + mAtm.mRootWindowContainer.moveActivityToPinnedRootTask(activityPip, "test"); final Task pinnedActivityTask = activityPip.getTask(); // Simulate pinnedActivityTask unintentionally added to recent during top activity resume. diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 52a80b01971d..7320c0bd4666 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -2005,10 +2005,10 @@ public class TransitionTests extends WindowTestsBase { @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOverrideAnimationOptionsToInfoIfNecessary_disableAnimOptionsPerChange() { - initializeOverrideAnimationOptionsTest(); + ActivityRecord r = initializeOverrideAnimationOptionsTest(); TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions .makeCommonAnimOptions("testPackage"); - mTransition.setOverrideAnimation(options, null /* startCallback */, + mTransition.setOverrideAnimation(options, r, null /* startCallback */, null /* finishCallback */); mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); @@ -2019,10 +2019,10 @@ public class TransitionTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOverrideAnimationOptionsToInfoIfNecessary_fromStyleAnimOptions() { - initializeOverrideAnimationOptionsTest(); + ActivityRecord r = initializeOverrideAnimationOptionsTest(); TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions .makeCommonAnimOptions("testPackage"); - mTransition.setOverrideAnimation(options, null /* startCallback */, + mTransition.setOverrideAnimation(options, r, null /* startCallback */, null /* finishCallback */); mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); @@ -2045,10 +2045,10 @@ public class TransitionTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOverrideAnimationOptionsToInfoIfNecessary_sceneAnimOptions() { - initializeOverrideAnimationOptionsTest(); + ActivityRecord r = initializeOverrideAnimationOptionsTest(); TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions .makeSceneTransitionAnimOptions(); - mTransition.setOverrideAnimation(options, null /* startCallback */, + mTransition.setOverrideAnimation(options, r, null /* startCallback */, null /* finishCallback */); mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); @@ -2071,10 +2071,10 @@ public class TransitionTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOverrideAnimationOptionsToInfoIfNecessary_crossProfileAnimOptions() { - initializeOverrideAnimationOptionsTest(); + ActivityRecord r = initializeOverrideAnimationOptionsTest(); TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions .makeCrossProfileAnimOptions(); - mTransition.setOverrideAnimation(options, null /* startCallback */, + mTransition.setOverrideAnimation(options, r, null /* startCallback */, null /* finishCallback */); final TransitionInfo.Change displayChange = mInfo.getChanges().get(0); @@ -2099,13 +2099,13 @@ public class TransitionTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOverrideAnimationOptionsToInfoIfNecessary_customAnimOptions() { - initializeOverrideAnimationOptionsTest(); + ActivityRecord r = initializeOverrideAnimationOptionsTest(); TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions .makeCustomAnimOptions("testPackage", Resources.ID_NULL, TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, Color.GREEN, false /* overrideTaskTransition */); - mTransition.setOverrideAnimation(options, null /* startCallback */, + mTransition.setOverrideAnimation(options, r, null /* startCallback */, null /* finishCallback */); mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); @@ -2132,7 +2132,7 @@ public class TransitionTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOverrideAnimationOptionsToInfoIfNecessary_haveTaskFragmentAnimParams() { - initializeOverrideAnimationOptionsTest(); + ActivityRecord r = initializeOverrideAnimationOptionsTest(); final TaskFragment embeddedTf = mTransition.mTargets.get(2).mContainer.asTaskFragment(); embeddedTf.setAnimationParams(new TaskFragmentAnimationParams.Builder() @@ -2145,7 +2145,7 @@ public class TransitionTests extends WindowTestsBase { TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, Color.GREEN, false /* overrideTaskTransition */); - mTransition.setOverrideAnimation(options, null /* startCallback */, + mTransition.setOverrideAnimation(options, r, null /* startCallback */, null /* finishCallback */); final TransitionInfo.Change displayChange = mInfo.getChanges().get(0); @@ -2181,13 +2181,13 @@ public class TransitionTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE) @Test public void testOverrideAnimationOptionsToInfoIfNecessary_customAnimOptionsWithTaskOverride() { - initializeOverrideAnimationOptionsTest(); + ActivityRecord r = initializeOverrideAnimationOptionsTest(); TransitionInfo.AnimationOptions options = TransitionInfo.AnimationOptions .makeCustomAnimOptions("testPackage", Resources.ID_NULL, TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID, Color.GREEN, true /* overrideTaskTransition */); - mTransition.setOverrideAnimation(options, null /* startCallback */, + mTransition.setOverrideAnimation(options, r, null /* startCallback */, null /* finishCallback */); mTransition.overrideAnimationOptionsToInfoIfNecessary(mInfo); @@ -2213,7 +2213,7 @@ public class TransitionTests extends WindowTestsBase { options.getBackgroundColor(), activityChange.getBackgroundColor()); } - private void initializeOverrideAnimationOptionsTest() { + private ActivityRecord initializeOverrideAnimationOptionsTest() { mTransition = createTestTransition(TRANSIT_OPEN); // Test set AnimationOptions for Activity and Task. @@ -2241,6 +2241,7 @@ public class TransitionTests extends WindowTestsBase { embeddedTf.getAnimationLeash())); mInfo.addChange(new TransitionInfo.Change(null /* container */, nonEmbeddedActivity.getAnimationLeash())); + return nonEmbeddedActivity; } @Test diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 1ba496df5005..cba2eeae5c9f 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9994,6 +9994,17 @@ public class CarrierConfigManager { public static final String KEY_SATELLITE_ESOS_SUPPORTED_BOOL = "satellite_esos_supported_bool"; /** + * Indicate whether carrier roaming to satellite is using P2P SMS. + * + * This will need agreement with carriers before enabling this flag. + * + * The default value is false. + */ + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + public static final String KEY_SATELLITE_ROAMING_P2P_SMS_SUPPORTED_BOOL = + "satellite_roaming_p2p_sms_supported_bool"; + + /** * Defines the NIDD (Non-IP Data Delivery) APN to be used for carrier roaming to satellite * attachment. For more on NIDD, see 3GPP TS 29.542. * Note this config is the only source of truth regarding the definition of the APN. @@ -10075,7 +10086,35 @@ public class CarrierConfigManager { */ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) public static final String KEY_SATELLITE_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT = - "satellite_screen_off_inactivity_timeout_duration_sec_int"; + "satellite_screen_off_inactivity_timeout_sec_int"; + + /** + * An integer key holds the timeout duration in seconds used to determine whether to exit P2P + * SMS mode and start TN scanning. + * + * The timer is started when the device is not connected, user is not pointing to the + * satellite and no data transfer is happening. + * When the timer expires, the device will move to IDLE mode upon which TN scanning will start. + * + * The default value is 180 seconds. + */ + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + public static final String KEY_SATELLITE_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT = + "satellite_p2p_sms_inactivity_timeout_sec_int"; + + /** + * An integer key holds the timeout duration in seconds used to determine whether to exit ESOS + * mode and start TN scanning. + * + * The timer is started when the device is not connected, user is not pointing to the + * satellite and no data transfer is happening. + * When the timer expires, the device will move to IDLE mode upon which TN scanning will start. + * + * The default value is 600 seconds. + */ + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + public static final String KEY_SATELLITE_ESOS_INACTIVITY_TIMEOUT_SEC_INT = + "satellite_esos_inactivity_timeout_sec_int"; /** * Indicating whether DUN APN should be disabled when the device is roaming. In that case, @@ -11235,12 +11274,15 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_EMERGENCY_CALL_TO_SATELLITE_T911_HANDOVER_TIMEOUT_MILLIS_INT, (int) TimeUnit.SECONDS.toMillis(30)); sDefaults.putBoolean(KEY_SATELLITE_ESOS_SUPPORTED_BOOL, false); + sDefaults.putBoolean(KEY_SATELLITE_ROAMING_P2P_SMS_SUPPORTED_BOOL, false); sDefaults.putString(KEY_SATELLITE_NIDD_APN_NAME_STRING, ""); sDefaults.putInt(KEY_CARRIER_ROAMING_NTN_CONNECT_TYPE_INT, 0); sDefaults.putInt(KEY_CARRIER_ROAMING_NTN_EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_INT, SatelliteManager.EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911); sDefaults.putInt(KEY_CARRIER_SUPPORTED_SATELLITE_NOTIFICATION_HYSTERESIS_SEC_INT, 180); sDefaults.putInt(KEY_SATELLITE_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT, 30); + sDefaults.putInt(KEY_SATELLITE_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT, 180); + sDefaults.putInt(KEY_SATELLITE_ESOS_INACTIVITY_TIMEOUT_SEC_INT, 600); sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, ""); sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false); sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 6ef953c505cb..c5934a70610f 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -546,6 +546,16 @@ public final class SatelliteManager { public static final int EMERGENCY_CALL_TO_SATELLITE_HANDOVER_TYPE_T911 = 2; /** + * This intent will be broadcasted if there are any change to list of subscriber informations. + * This intent will be sent only to the app with component defined in + * config_satellite_carrier_roaming_esos_provisioned_class and package defined in + * config_satellite_gateway_service_package + * @hide + */ + public static final String ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED = + "android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED"; + + /** * Request to enable or disable the satellite modem and demo mode. * If satellite modem and cellular modem cannot work concurrently, * then this will disable the cellular modem if satellite modem is enabled, diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 7a4f40e471d2..97514599c0b1 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -50,21 +50,21 @@ bool less_than_type(const std::unique_ptr<ResourceTableType>& lhs, template <typename T> bool less_than_struct_with_name(const std::unique_ptr<T>& lhs, StringPiece rhs) { - return lhs->name.compare(0, lhs->name.size(), rhs.data(), rhs.size()) < 0; + return lhs->name < rhs; } template <typename T> bool greater_than_struct_with_name(StringPiece lhs, const std::unique_ptr<T>& rhs) { - return rhs->name.compare(0, rhs->name.size(), lhs.data(), lhs.size()) > 0; + return rhs->name > lhs; } template <typename T> struct NameEqualRange { bool operator()(const std::unique_ptr<T>& lhs, StringPiece rhs) const { - return less_than_struct_with_name<T>(lhs, rhs); + return less_than_struct_with_name(lhs, rhs); } bool operator()(StringPiece lhs, const std::unique_ptr<T>& rhs) const { - return greater_than_struct_with_name<T>(lhs, rhs); + return greater_than_struct_with_name(lhs, rhs); } }; @@ -74,7 +74,7 @@ bool less_than_struct_with_name_and_id(const T& lhs, if (lhs.id != rhs.second) { return lhs.id < rhs.second; } - return lhs.name.compare(0, lhs.name.size(), rhs.first.data(), rhs.first.size()) < 0; + return lhs.name < rhs.first; } template <typename T, typename Func, typename Elements> @@ -90,14 +90,16 @@ struct ConfigKey { StringPiece product; }; -template <typename T> -bool lt_config_key_ref(const T& lhs, const ConfigKey& rhs) { - int cmp = lhs->config.compare(*rhs.config); - if (cmp == 0) { - cmp = StringPiece(lhs->product).compare(rhs.product); +struct lt_config_key_ref { + template <typename T> + bool operator()(const T& lhs, const ConfigKey& rhs) const noexcept { + int cmp = lhs->config.compare(*rhs.config); + if (cmp == 0) { + cmp = lhs->product.compare(rhs.product); + } + return cmp < 0; } - return cmp < 0; -} +}; } // namespace @@ -159,10 +161,10 @@ ResourceEntry* ResourceTableType::FindOrCreateEntry(android::StringPiece name) { ResourceConfigValue* ResourceEntry::FindValue(const ConfigDescription& config, android::StringPiece product) { auto iter = std::lower_bound(values.begin(), values.end(), ConfigKey{&config, product}, - lt_config_key_ref<std::unique_ptr<ResourceConfigValue>>); + lt_config_key_ref()); if (iter != values.end()) { ResourceConfigValue* value = iter->get(); - if (value->config == config && StringPiece(value->product) == product) { + if (value->config == config && value->product == product) { return value; } } @@ -172,10 +174,10 @@ ResourceConfigValue* ResourceEntry::FindValue(const ConfigDescription& config, const ResourceConfigValue* ResourceEntry::FindValue(const android::ConfigDescription& config, android::StringPiece product) const { auto iter = std::lower_bound(values.begin(), values.end(), ConfigKey{&config, product}, - lt_config_key_ref<std::unique_ptr<ResourceConfigValue>>); + lt_config_key_ref()); if (iter != values.end()) { ResourceConfigValue* value = iter->get(); - if (value->config == config && StringPiece(value->product) == product) { + if (value->config == config && value->product == product) { return value; } } @@ -185,10 +187,10 @@ const ResourceConfigValue* ResourceEntry::FindValue(const android::ConfigDescrip ResourceConfigValue* ResourceEntry::FindOrCreateValue(const ConfigDescription& config, StringPiece product) { auto iter = std::lower_bound(values.begin(), values.end(), ConfigKey{&config, product}, - lt_config_key_ref<std::unique_ptr<ResourceConfigValue>>); + lt_config_key_ref()); if (iter != values.end()) { ResourceConfigValue* value = iter->get(); - if (value->config == config && StringPiece(value->product) == product) { + if (value->config == config && value->product == product) { return value; } } @@ -199,36 +201,21 @@ ResourceConfigValue* ResourceEntry::FindOrCreateValue(const ConfigDescription& c std::vector<ResourceConfigValue*> ResourceEntry::FindAllValues(const ConfigDescription& config) { std::vector<ResourceConfigValue*> results; - - auto iter = values.begin(); + auto iter = + std::lower_bound(values.begin(), values.end(), ConfigKey{&config, ""}, lt_config_key_ref()); for (; iter != values.end(); ++iter) { ResourceConfigValue* value = iter->get(); - if (value->config == config) { - results.push_back(value); - ++iter; + if (value->config != config) { break; } - } - - for (; iter != values.end(); ++iter) { - ResourceConfigValue* value = iter->get(); - if (value->config == config) { - results.push_back(value); - } + results.push_back(value); } return results; } bool ResourceEntry::HasDefaultValue() const { - const ConfigDescription& default_config = ConfigDescription::DefaultConfig(); - // The default config should be at the top of the list, since the list is sorted. - for (auto& config_value : values) { - if (config_value->config == default_config) { - return true; - } - } - return false; + return !values.empty() && values.front()->config == ConfigDescription::DefaultConfig(); } ResourceTable::CollisionResult ResourceTable::ResolveFlagCollision(FlagStatus existing, @@ -364,14 +351,14 @@ struct SortedVectorInserter : public Comparer { if (found) { return &*it; } - return &*el.insert(it, std::forward<T>(value)); + return &*el.insert(it, std::move(value)); } }; struct PackageViewComparer { bool operator()(const ResourceTablePackageView& lhs, const ResourceTablePackageView& rhs) { return less_than_struct_with_name_and_id<ResourceTablePackageView, uint8_t>( - lhs, std::make_pair(rhs.name, rhs.id)); + lhs, std::tie(rhs.name, rhs.id)); } }; @@ -384,7 +371,7 @@ struct TypeViewComparer { struct EntryViewComparer { bool operator()(const ResourceTableEntryView& lhs, const ResourceTableEntryView& rhs) { return less_than_struct_with_name_and_id<ResourceTableEntryView, uint16_t>( - lhs, std::make_pair(rhs.name, rhs.id)); + lhs, std::tie(rhs.name, rhs.id)); } }; @@ -429,10 +416,10 @@ void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePacka const ResourceConfigValue* ResourceTableEntryView::FindValue(const ConfigDescription& config, android::StringPiece product) const { auto iter = std::lower_bound(values.begin(), values.end(), ConfigKey{&config, product}, - lt_config_key_ref<const ResourceConfigValue*>); + lt_config_key_ref()); if (iter != values.end()) { const ResourceConfigValue* value = *iter; - if (value->config == config && StringPiece(value->product) == product) { + if (value->config == config && value->product == product) { return value; } } @@ -615,11 +602,15 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) result = ResolveValueCollision(config_value->value.get(), res.value.get()); } switch (result) { - case CollisionResult::kKeepBoth: + case CollisionResult::kKeepBoth: { // Insert the value ignoring for duplicate configurations - entry->values.push_back(util::make_unique<ResourceConfigValue>(res.config, res.product)); - entry->values.back()->value = std::move(res.value); + auto it = entry->values.insert( + std::lower_bound(entry->values.begin(), entry->values.end(), + ConfigKey{&res.config, res.product}, lt_config_key_ref()), + util::make_unique<ResourceConfigValue>(res.config, res.product)); + (*it)->value = std::move(res.value); break; + } case CollisionResult::kTakeNew: // Take the incoming value. |