diff options
705 files changed, 20478 insertions, 7343 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index 86ed06bf4e3d..29df80fda33d 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -105,4 +105,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "include_trace_tag_in_job_name" + namespace: "backstage_power" + description: "Add the trace tag to the job name" + bug: "354795473" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index aaf69864fe97..2d069f934d0d 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -674,6 +674,12 @@ public final class JobStatus { this.job = job; StringBuilder batteryName = new StringBuilder(); + if (com.android.server.job.Flags.includeTraceTagInJobName()) { + final String filteredTraceTag = this.getFilteredTraceTag(); + if (filteredTraceTag != null) { + batteryName.append("#").append(filteredTraceTag).append("#"); + } + } if (namespace != null) { batteryName.append("@").append(namespace).append("@"); } diff --git a/core/api/current.txt b/core/api/current.txt index d4ed533cad9e..5c0ecf72030f 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -100,6 +100,9 @@ package android { field public static final String EXECUTE_APP_ACTION = "android.permission.EXECUTE_APP_ACTION"; field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXECUTE_APP_FUNCTIONS = "android.permission.EXECUTE_APP_FUNCTIONS"; field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_TRACKING_COARSE = "android.permission.EYE_TRACKING_COARSE"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_TRACKING_FINE = "android.permission.EYE_TRACKING_FINE"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String FACE_TRACKING = "android.permission.FACE_TRACKING"; field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST"; field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE"; field public static final String FOREGROUND_SERVICE_CAMERA = "android.permission.FOREGROUND_SERVICE_CAMERA"; @@ -120,6 +123,8 @@ package android { field public static final String GET_PACKAGE_SIZE = "android.permission.GET_PACKAGE_SIZE"; field @Deprecated public static final String GET_TASKS = "android.permission.GET_TASKS"; field public static final String GLOBAL_SEARCH = "android.permission.GLOBAL_SEARCH"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String HAND_TRACKING = "android.permission.HAND_TRACKING"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String HEAD_TRACKING = "android.permission.HEAD_TRACKING"; field public static final String HIDE_OVERLAY_WINDOWS = "android.permission.HIDE_OVERLAY_WINDOWS"; field public static final String HIGH_SAMPLING_RATE_SENSORS = "android.permission.HIGH_SAMPLING_RATE_SENSORS"; field public static final String INSTALL_LOCATION_PROVIDER = "android.permission.INSTALL_LOCATION_PROVIDER"; @@ -295,6 +300,8 @@ package android { field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY"; field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES"; field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String SCENE_UNDERSTANDING_COARSE = "android.permission.SCENE_UNDERSTANDING_COARSE"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String SCENE_UNDERSTANDING_FINE = "android.permission.SCENE_UNDERSTANDING_FINE"; field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM"; field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE"; field public static final String SEND_SMS = "android.permission.SEND_SMS"; @@ -362,6 +369,8 @@ package android { field public static final String SENSORS = "android.permission-group.SENSORS"; field public static final String SMS = "android.permission-group.SMS"; field public static final String STORAGE = "android.permission-group.STORAGE"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING = "android.permission-group.XR_TRACKING"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING_SENSITIVE = "android.permission-group.XR_TRACKING_SENSITIVE"; } public final class R { @@ -5479,37 +5488,37 @@ package android.app { method public android.net.Uri getConditionId(); method @Nullable public android.content.ComponentName getConfigurationActivity(); method public long getCreationTime(); - method @FlaggedApi("android.app.modes_api") @Nullable public android.service.notification.ZenDeviceEffects getDeviceEffects(); - method @FlaggedApi("android.app.modes_api") @DrawableRes public int getIconResId(); + method @Nullable public android.service.notification.ZenDeviceEffects getDeviceEffects(); + method @DrawableRes public int getIconResId(); method public int getInterruptionFilter(); method public String getName(); method public android.content.ComponentName getOwner(); - method @FlaggedApi("android.app.modes_api") @Nullable public String getTriggerDescription(); - method @FlaggedApi("android.app.modes_api") public int getType(); + method @Nullable public String getTriggerDescription(); + method public int getType(); method @Nullable public android.service.notification.ZenPolicy getZenPolicy(); method public boolean isEnabled(); - method @FlaggedApi("android.app.modes_api") public boolean isManualInvocationAllowed(); + method public boolean isManualInvocationAllowed(); method public void setConditionId(android.net.Uri); method public void setConfigurationActivity(@Nullable android.content.ComponentName); - method @FlaggedApi("android.app.modes_api") public void setDeviceEffects(@Nullable android.service.notification.ZenDeviceEffects); + method public void setDeviceEffects(@Nullable android.service.notification.ZenDeviceEffects); method public void setEnabled(boolean); method public void setInterruptionFilter(int); method public void setName(String); method public void setZenPolicy(@Nullable android.service.notification.ZenPolicy); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.AutomaticZenRule> CREATOR; - field @FlaggedApi("android.app.modes_api") public static final int TYPE_BEDTIME = 3; // 0x3 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_DRIVING = 4; // 0x4 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_IMMERSIVE = 5; // 0x5 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_MANAGED = 7; // 0x7 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_OTHER = 0; // 0x0 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_CALENDAR = 2; // 0x2 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_SCHEDULE_TIME = 1; // 0x1 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_THEATER = 6; // 0x6 - field @FlaggedApi("android.app.modes_api") public static final int TYPE_UNKNOWN = -1; // 0xffffffff - } - - @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder { + field public static final int TYPE_BEDTIME = 3; // 0x3 + field public static final int TYPE_DRIVING = 4; // 0x4 + field public static final int TYPE_IMMERSIVE = 5; // 0x5 + field public static final int TYPE_MANAGED = 7; // 0x7 + field public static final int TYPE_OTHER = 0; // 0x0 + field public static final int TYPE_SCHEDULE_CALENDAR = 2; // 0x2 + field public static final int TYPE_SCHEDULE_TIME = 1; // 0x1 + field public static final int TYPE_THEATER = 6; // 0x6 + field public static final int TYPE_UNKNOWN = -1; // 0xffffffff + } + + public static final class AutomaticZenRule.Builder { ctor public AutomaticZenRule.Builder(@NonNull android.app.AutomaticZenRule); ctor public AutomaticZenRule.Builder(@NonNull String, @NonNull android.net.Uri); method @NonNull public android.app.AutomaticZenRule build(); @@ -7127,7 +7136,7 @@ package android.app { public class NotificationManager { method public String addAutomaticZenRule(android.app.AutomaticZenRule); - method @FlaggedApi("android.app.modes_api") public boolean areAutomaticZenRulesUserManaged(); + method public boolean areAutomaticZenRulesUserManaged(); method @Deprecated public boolean areBubblesAllowed(); method public boolean areBubblesEnabled(); method public boolean areNotificationsEnabled(); @@ -7147,7 +7156,7 @@ package android.app { method public void deleteNotificationChannelGroup(String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.app.AutomaticZenRule getAutomaticZenRule(String); - method @FlaggedApi("android.app.modes_api") public int getAutomaticZenRuleState(@NonNull String); + method public int getAutomaticZenRuleState(@NonNull String); method public java.util.Map<java.lang.String,android.app.AutomaticZenRule> getAutomaticZenRules(); method public int getBubblePreference(); method @NonNull public android.app.NotificationManager.Policy getConsolidatedNotificationPolicy(); @@ -7176,14 +7185,14 @@ package android.app { field public static final String ACTION_APP_BLOCK_STATE_CHANGED = "android.app.action.APP_BLOCK_STATE_CHANGED"; field public static final String ACTION_AUTOMATIC_ZEN_RULE = "android.app.action.AUTOMATIC_ZEN_RULE"; field public static final String ACTION_AUTOMATIC_ZEN_RULE_STATUS_CHANGED = "android.app.action.AUTOMATIC_ZEN_RULE_STATUS_CHANGED"; - field @FlaggedApi("android.app.modes_api") public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED = "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED"; + field public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED = "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED"; field public static final String ACTION_INTERRUPTION_FILTER_CHANGED = "android.app.action.INTERRUPTION_FILTER_CHANGED"; field public static final String ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED = "android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED"; field public static final String ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED = "android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED"; field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED = "android.app.action.NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED"; field public static final String ACTION_NOTIFICATION_POLICY_CHANGED = "android.app.action.NOTIFICATION_POLICY_CHANGED"; - field @FlaggedApi("android.app.modes_api") public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4; // 0x4 - field @FlaggedApi("android.app.modes_api") public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5; // 0x5 + field public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4; // 0x4 + field public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5; // 0x5 field public static final int AUTOMATIC_RULE_STATUS_DISABLED = 2; // 0x2 field public static final int AUTOMATIC_RULE_STATUS_ENABLED = 1; // 0x1 field public static final int AUTOMATIC_RULE_STATUS_REMOVED = 3; // 0x3 @@ -7197,7 +7206,7 @@ package android.app { field public static final String EXTRA_BLOCKED_STATE = "android.app.extra.BLOCKED_STATE"; field public static final String EXTRA_NOTIFICATION_CHANNEL_GROUP_ID = "android.app.extra.NOTIFICATION_CHANNEL_GROUP_ID"; field public static final String EXTRA_NOTIFICATION_CHANNEL_ID = "android.app.extra.NOTIFICATION_CHANNEL_ID"; - field @FlaggedApi("android.app.modes_api") public static final String EXTRA_NOTIFICATION_POLICY = "android.app.extra.NOTIFICATION_POLICY"; + field public static final String EXTRA_NOTIFICATION_POLICY = "android.app.extra.NOTIFICATION_POLICY"; field public static final int IMPORTANCE_DEFAULT = 3; // 0x3 field public static final int IMPORTANCE_HIGH = 4; // 0x4 field public static final int IMPORTANCE_LOW = 2; // 0x2 @@ -9188,6 +9197,14 @@ package android.app.blob { } +package android.app.contextualsearch { + + @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public final class ContextualSearchManager { + method @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public void startContextualSearch(); + } + +} + package android.app.jank { @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public final class AppJankStats { @@ -17604,6 +17621,7 @@ package android.graphics { method public void setIntUniform(@NonNull String, int, int, int); method public void setIntUniform(@NonNull String, int, int, int, int); method public void setIntUniform(@NonNull String, @NonNull int[]); + method @FlaggedApi("com.android.graphics.hwui.flags.shader_color_space") public void setWorkingColorSpace(@Nullable android.graphics.ColorSpace); } @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode { @@ -25039,8 +25057,10 @@ package android.media { method public final void notifySessionCreated(long, @NonNull android.media.RoutingSessionInfo); method public final void notifySessionReleased(@NonNull String); method public final void notifySessionUpdated(@NonNull android.media.RoutingSessionInfo); + method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @Nullable @RequiresPermission("android.permission.MODIFY_AUDIO_ROUTING") public final android.media.MediaRoute2ProviderService.MediaStreams notifySystemRoutingSessionCreated(long, @NonNull android.media.RoutingSessionInfo, @NonNull android.media.MediaRoute2ProviderService.MediaStreamsFormats); method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); method public abstract void onCreateSession(long, @NonNull String, @NonNull String, @Nullable android.os.Bundle); + method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public void onCreateSystemRoutingSession(long, @NonNull String, @NonNull android.media.MediaRoute2ProviderService.SystemRoutingSessionParams); method public abstract void onDeselectRoute(long, @NonNull String, @NonNull String); method public void onDiscoveryPreferenceChanged(@NonNull android.media.RouteDiscoveryPreference); method public abstract void onReleaseSession(long, @NonNull String); @@ -25048,15 +25068,44 @@ package android.media { method public abstract void onSetRouteVolume(long, @NonNull String, int); method public abstract void onSetSessionVolume(long, @NonNull String, int); method public abstract void onTransferToRoute(long, @NonNull String, @NonNull String); + field @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final String CATEGORY_SYSTEM_MEDIA = "android.media.MediaRoute2ProviderService.SYSTEM_MEDIA"; + field @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final int REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA = 6; // 0x6 field public static final int REASON_INVALID_COMMAND = 4; // 0x4 field public static final int REASON_NETWORK_ERROR = 2; // 0x2 field public static final int REASON_REJECTED = 1; // 0x1 field public static final int REASON_ROUTE_NOT_AVAILABLE = 3; // 0x3 + field @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final int REASON_UNIMPLEMENTED = 5; // 0x5 field public static final int REASON_UNKNOWN_ERROR = 0; // 0x0 field public static final long REQUEST_ID_NONE = 0L; // 0x0L field public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService"; } + @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.MediaStreams { + method @Nullable public android.media.AudioRecord getAudioRecord(); + } + + @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.MediaStreamsFormats { + method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @Nullable public android.media.AudioFormat getAudioFormat(); + } + + @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.MediaStreamsFormats.Builder { + ctor public MediaRoute2ProviderService.MediaStreamsFormats.Builder(); + method @NonNull public android.media.MediaRoute2ProviderService.MediaStreamsFormats build(); + method @NonNull public android.media.MediaRoute2ProviderService.MediaStreamsFormats.Builder setAudioFormat(@NonNull android.media.AudioFormat); + } + + @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") public static final class MediaRoute2ProviderService.SystemRoutingSessionParams { + method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @NonNull public android.os.Bundle getExtras(); + method @FlaggedApi("com.android.media.flags.enable_mirroring_in_media_router_2") @NonNull public String getPackageName(); + } + + public static final class MediaRoute2ProviderService.SystemRoutingSessionParams.Builder { + ctor public MediaRoute2ProviderService.SystemRoutingSessionParams.Builder(); + method @NonNull public android.media.MediaRoute2ProviderService.SystemRoutingSessionParams build(); + method @NonNull public android.media.MediaRoute2ProviderService.SystemRoutingSessionParams.Builder setExtras(@NonNull android.os.Bundle); + method @NonNull public android.media.MediaRoute2ProviderService.SystemRoutingSessionParams.Builder setPackageName(@NonNull String); + } + public class MediaRouter { method public void addCallback(int, android.media.MediaRouter.Callback); method public void addCallback(int, android.media.MediaRouter.Callback, int); @@ -38232,7 +38281,7 @@ package android.provider { field public static final String ACTION_APP_OPEN_BY_DEFAULT_SETTINGS = "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS"; field public static final String ACTION_APP_SEARCH_SETTINGS = "android.settings.APP_SEARCH_SETTINGS"; field public static final String ACTION_APP_USAGE_SETTINGS = "android.settings.action.APP_USAGE_SETTINGS"; - field @FlaggedApi("android.app.modes_api") public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS"; + field public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS"; field public static final String ACTION_AUTO_ROTATE_SETTINGS = "android.settings.AUTO_ROTATE_SETTINGS"; field public static final String ACTION_BATTERY_SAVER_SETTINGS = "android.settings.BATTERY_SAVER_SETTINGS"; field public static final String ACTION_BIOMETRIC_ENROLL = "android.settings.BIOMETRIC_ENROLL"; @@ -38326,7 +38375,7 @@ package android.provider { field public static final String EXTRA_AIRPLANE_MODE_ENABLED = "airplane_mode_enabled"; field public static final String EXTRA_APP_PACKAGE = "android.provider.extra.APP_PACKAGE"; field public static final String EXTRA_AUTHORITIES = "authorities"; - field @FlaggedApi("android.app.modes_api") public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID"; + field public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID"; field public static final String EXTRA_BATTERY_SAVER_MODE_ENABLED = "android.settings.extra.battery_saver_mode_enabled"; field public static final String EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED = "android.provider.extra.BIOMETRIC_AUTHENTICATORS_ALLOWED"; field public static final String EXTRA_CHANNEL_FILTER_LIST = "android.provider.extra.CHANNEL_FILTER_LIST"; @@ -42118,9 +42167,9 @@ package android.service.notification { public final class Condition implements android.os.Parcelable { ctor public Condition(android.net.Uri, String, int); - ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, int, int); + ctor public Condition(@Nullable android.net.Uri, @Nullable String, int, int); ctor public Condition(android.net.Uri, String, String, String, int, int, int); - ctor @FlaggedApi("android.app.modes_api") public Condition(@Nullable android.net.Uri, @Nullable String, @Nullable String, @Nullable String, int, int, int, int); + ctor public Condition(@Nullable android.net.Uri, @Nullable String, @Nullable String, @Nullable String, int, int, int, int); ctor public Condition(android.os.Parcel); method public android.service.notification.Condition copy(); method public int describeContents(); @@ -42133,10 +42182,10 @@ package android.service.notification { field public static final int FLAG_RELEVANT_ALWAYS = 2; // 0x2 field public static final int FLAG_RELEVANT_NOW = 1; // 0x1 field public static final String SCHEME = "condition"; - field @FlaggedApi("android.app.modes_api") public static final int SOURCE_CONTEXT = 3; // 0x3 - field @FlaggedApi("android.app.modes_api") public static final int SOURCE_SCHEDULE = 2; // 0x2 - field @FlaggedApi("android.app.modes_api") public static final int SOURCE_UNKNOWN = 0; // 0x0 - field @FlaggedApi("android.app.modes_api") public static final int SOURCE_USER_ACTION = 1; // 0x1 + field public static final int SOURCE_CONTEXT = 3; // 0x3 + field public static final int SOURCE_SCHEDULE = 2; // 0x2 + field public static final int SOURCE_UNKNOWN = 0; // 0x0 + field public static final int SOURCE_USER_ACTION = 1; // 0x1 field public static final int STATE_ERROR = 3; // 0x3 field public static final int STATE_FALSE = 0; // 0x0 field public static final int STATE_TRUE = 1; // 0x1 @@ -42146,7 +42195,7 @@ package android.service.notification { field public final android.net.Uri id; field public final String line1; field public final String line2; - field @FlaggedApi("android.app.modes_api") public final int source; + field public final int source; field public final int state; field public final String summary; } @@ -42317,7 +42366,7 @@ package android.service.notification { field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR; } - @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable { + public final class ZenDeviceEffects implements android.os.Parcelable { method public int describeContents(); method public boolean shouldDimWallpaper(); method public boolean shouldDisplayGrayscale(); @@ -42327,7 +42376,7 @@ package android.service.notification { field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.ZenDeviceEffects> CREATOR; } - @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder { + public static final class ZenDeviceEffects.Builder { ctor public ZenDeviceEffects.Builder(); ctor public ZenDeviceEffects.Builder(@NonNull android.service.notification.ZenDeviceEffects); method @NonNull public android.service.notification.ZenDeviceEffects build(); @@ -42349,7 +42398,7 @@ package android.service.notification { method public int getPriorityCategoryReminders(); method public int getPriorityCategoryRepeatCallers(); method public int getPriorityCategorySystem(); - method @FlaggedApi("android.app.modes_api") public int getPriorityChannelsAllowed(); + method public int getPriorityChannelsAllowed(); method public int getPriorityConversationSenders(); method public int getPriorityMessageSenders(); method public int getVisualEffectAmbient(); @@ -42384,7 +42433,7 @@ package android.service.notification { method @NonNull public android.service.notification.ZenPolicy.Builder allowEvents(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowMedia(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowMessages(int); - method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean); + method @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowReminders(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowRepeatCallers(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowSystem(boolean); @@ -45122,6 +45171,7 @@ package android.telephony { field public static final String KEY_RTT_UPGRADE_SUPPORTED_BOOL = "rtt_upgrade_supported_bool"; field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT = "satellite_connected_notification_throttle_millis_int"; 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.satellite_system_apis") public static final String KEY_SATELLITE_DATA_SUPPORT_MODE_INT = "satellite_data_support_mode_int"; field @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") public static final String KEY_SATELLITE_DISPLAY_NAME_STRING = "satellite_display_name_string"; @@ -48694,6 +48744,7 @@ package android.telephony.satellite { @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager { method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void registerStateChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateChangeListener); method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void unregisterStateChangeListener(@NonNull android.telephony.satellite.SatelliteStateChangeListener); + field @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED = "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"; } @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public interface SatelliteStateChangeListener { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 76cce7439454..0d5ec199b953 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -7,7 +7,7 @@ package android { field public static final String ACCESS_BROADCAST_RADIO = "android.permission.ACCESS_BROADCAST_RADIO"; field public static final String ACCESS_BROADCAST_RESPONSE_STATS = "android.permission.ACCESS_BROADCAST_RESPONSE_STATS"; field public static final String ACCESS_CACHE_FILESYSTEM = "android.permission.ACCESS_CACHE_FILESYSTEM"; - field @FlaggedApi("android.app.contextualsearch.flags.enable_service") public static final String ACCESS_CONTEXTUAL_SEARCH = "android.permission.ACCESS_CONTEXTUAL_SEARCH"; + field public static final String ACCESS_CONTEXTUAL_SEARCH = "android.permission.ACCESS_CONTEXTUAL_SEARCH"; field public static final String ACCESS_CONTEXT_HUB = "android.permission.ACCESS_CONTEXT_HUB"; field public static final String ACCESS_DRM_CERTIFICATES = "android.permission.ACCESS_DRM_CERTIFICATES"; field @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public static final String ACCESS_FINE_POWER_MONITORS = "android.permission.ACCESS_FINE_POWER_MONITORS"; @@ -151,6 +151,8 @@ package android { field @FlaggedApi("android.content.pm.emergency_install_permission") public static final String EMERGENCY_INSTALL_PACKAGES = "android.permission.EMERGENCY_INSTALL_PACKAGES"; field public static final String ENTER_CAR_MODE_PRIORITIZED = "android.permission.ENTER_CAR_MODE_PRIORITIZED"; field public static final String EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS = "android.permission.EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String EYE_CALIBRATION = "android.permission.EYE_CALIBRATION"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String FACE_TRACKING_CALIBRATION = "android.permission.FACE_TRACKING_CALIBRATION"; field public static final String FORCE_BACK = "android.permission.FORCE_BACK"; field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; field public static final String GET_APP_METADATA = "android.permission.GET_APP_METADATA"; @@ -168,6 +170,7 @@ package android { field public static final String HARDWARE_TEST = "android.permission.HARDWARE_TEST"; field public static final String HDMI_CEC = "android.permission.HDMI_CEC"; field @Deprecated public static final String HIDE_NON_SYSTEM_OVERLAY_WINDOWS = "android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String IMPORT_XR_ANCHOR = "android.permission.IMPORT_XR_ANCHOR"; field public static final String INJECT_EVENTS = "android.permission.INJECT_EVENTS"; field @FlaggedApi("android.content.pm.sdk_dependency_installer") public static final String INSTALL_DEPENDENCY_SHARED_LIBRARIES = "android.permission.INSTALL_DEPENDENCY_SHARED_LIBRARIES"; field public static final String INSTALL_DPC_PACKAGES = "android.permission.INSTALL_DPC_PACKAGES"; @@ -450,6 +453,7 @@ package android { field public static final String WRITE_SECURITY_LOG = "android.permission.WRITE_SECURITY_LOG"; field public static final String WRITE_SMS = "android.permission.WRITE_SMS"; field @FlaggedApi("android.provider.user_keys") public static final String WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS = "android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"; + field @FlaggedApi("android.xr.xr_manifest_entries") public static final String XR_TRACKING_IN_BACKGROUND = "android.permission.XR_TRACKING_IN_BACKGROUND"; } public static final class Manifest.permission_group { @@ -2231,7 +2235,7 @@ package android.app.contextualsearch { field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.CallbackToken> CREATOR; } - public final class ContextualSearchManager { + @FlaggedApi("android.app.contextualsearch.flags.self_invocation") public final class ContextualSearchManager { method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int); field public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH = "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH"; field public static final int ENTRYPOINT_LONG_PRESS_HOME = 2; // 0x2 @@ -2934,6 +2938,14 @@ package android.app.smartspace.uitemplatedata { } +package android.app.supervision { + + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { + method @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public boolean isSupervisionEnabled(); + } + +} + package android.app.time { public final class Capabilities { @@ -3797,6 +3809,7 @@ package android.content { field public static final String SHARED_CONNECTIVITY_SERVICE = "shared_connectivity"; field public static final String SMARTSPACE_SERVICE = "smartspace"; field public static final String STATS_MANAGER = "stats"; + field @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public static final String SUPERVISION_SERVICE = "supervision"; field public static final String SYSTEM_CONFIG_SERVICE = "system_config"; field public static final String SYSTEM_UPDATE_SERVICE = "system_update"; field @FlaggedApi("com.android.net.thread.platform.flags.thread_enabled_platform") public static final String THREAD_NETWORK_SERVICE = "thread_network"; @@ -18580,6 +18593,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_25q4_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatelliteDataOptimizedApps(); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons(); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 9e9e3c2f13c1..975c2c27cb22 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -393,25 +393,25 @@ package android.app { } public class NotificationManager { - method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean); + method @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean); method @FlaggedApi("android.service.notification.notification_classification") public void allowAssistantAdjustment(@NonNull String); method public void cleanUpCallersAfter(long); method @FlaggedApi("android.service.notification.notification_classification") public void disallowAssistantAdjustment(@NonNull String); - method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy(); + method @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy(); method public android.content.ComponentName getEffectsSuppressor(); method @FlaggedApi("android.service.notification.notification_classification") @NonNull public java.util.Set<java.lang.String> getUnsupportedAdjustmentTypes(); method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String); - method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean); + method public boolean removeAutomaticZenRule(@NonNull String, boolean); method @FlaggedApi("android.service.notification.notification_classification") public void setAssistantAdjustmentKeyTypeState(int, boolean); method @FlaggedApi("android.app.api_rich_ongoing") public void setCanPostPromotedNotifications(@NonNull String, int, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean); - method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean); + method public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean); method public void updateNotificationChannel(@NonNull String, int, @NonNull android.app.NotificationChannel); } public static class NotificationManager.Policy implements android.os.Parcelable { - method @FlaggedApi("android.app.modes_api") public boolean allowPriorityChannels(); + method public boolean allowPriorityChannels(); } public final class PendingIntent implements android.os.Parcelable { @@ -478,8 +478,8 @@ package android.app { method public void destroy(); method @NonNull public java.util.Set<java.lang.String> getAdoptedShellPermissions(); method @Deprecated public boolean grantRuntimePermission(String, String, android.os.UserHandle); - method public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean); - method public void injectInputEventToInputFilter(@NonNull android.view.InputEvent); + method @Deprecated public boolean injectInputEvent(@NonNull android.view.InputEvent, boolean, boolean); + method @Deprecated public void injectInputEventToInputFilter(@NonNull android.view.InputEvent); method public boolean isNodeInCache(@NonNull android.view.accessibility.AccessibilityNodeInfo); method public void removeOverridePermissionState(int, @NonNull String); method @Deprecated public boolean revokeRuntimePermission(String, String, android.os.UserHandle); @@ -490,15 +490,15 @@ package android.app { } public class UiModeManager { - method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay(); + method @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay(); method public boolean isNightModeLocked(); method public boolean isUiModeLocked(); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int); - field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea - field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9 - field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8 - field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff + field public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea + field public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9 + field public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8 + field public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1 field public static final int PROJECTION_TYPE_NONE = 0; // 0x0 @@ -850,6 +850,14 @@ package android.app.prediction { } +package android.app.supervision { + + @FlaggedApi("android.app.supervision.flags.supervision_manager_apis") public class SupervisionManager { + method public void setSupervisionEnabled(boolean); + } + +} + package android.app.usage { public class StorageStatsManager { @@ -1716,7 +1724,7 @@ package android.hardware.display { } public final class ColorDisplayManager { - method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated(); + method @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated(); } public final class DisplayManager { @@ -3237,16 +3245,16 @@ package android.service.notification { method @Deprecated public boolean isBound(); } - @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable { + public final class ZenDeviceEffects implements android.os.Parcelable { method @NonNull public java.util.Set<java.lang.String> getExtraEffects(); } - @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder { + public static final class ZenDeviceEffects.Builder { method @NonNull public android.service.notification.ZenDeviceEffects.Builder setExtraEffects(@NonNull java.util.Set<java.lang.String>); } public final class ZenPolicy implements android.os.Parcelable { - method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy); + method @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy); } public static final class ZenPolicy.Builder { diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 252d23f69400..ee9c64f97382 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -292,13 +292,15 @@ import java.util.function.Consumer; * to the user, it must be completely restarted and restored to its previous state.</li> * </ul> * - * <p>The following diagram shows the important state paths of an Activity. + * <p>The following diagram shows the important state paths of an activity. * The square rectangles represent callback methods you can implement to - * perform operations when the Activity moves between states. The colored - * ovals are major states the Activity can be in.</p> + * perform operations when the activity moves between states. The colored + * ovals are major states the activity can be in.</p> * - * <p><img src="../../../images/activity_lifecycle.png" - * alt="State diagram for an Android Activity Lifecycle." border="0" /></p> + * <p><img class="invert" + * style="display: block; margin: auto;" + * src="../../../images/activity_lifecycle.png" + * alt="State diagram for the Android activity lifecycle." /></p> * * <p>There are three key loops you may be interested in monitoring within your * activity: @@ -505,7 +507,7 @@ import java.util.function.Consumer; * changes.</p> * * <p>Unless you specify otherwise, a configuration change (such as a change - * in screen orientation, language, input devices, etc) will cause your + * in screen orientation, language, input devices, etc.) will cause your * current activity to be <em>destroyed</em>, going through the normal activity * lifecycle process of {@link #onPause}, * {@link #onStop}, and {@link #onDestroy} as appropriate. If the activity @@ -1838,7 +1840,7 @@ public class Activity extends ContextThemeWrapper * * <p>You can call {@link #finish} from within this function, in * which case onDestroy() will be immediately called after {@link #onCreate} without any of the - * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc) + * rest of the activity lifecycle ({@link #onStart}, {@link #onResume}, {@link #onPause}, etc.) * executing. * * <p><em>Derived classes must call through to the super class's @@ -2132,7 +2134,7 @@ public class Activity extends ContextThemeWrapper * * <p>You can call {@link #finish} from within this function, in * which case {@link #onStop} will be immediately called after {@link #onStart} without the - * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc) executing. + * lifecycle transitions in-between ({@link #onResume}, {@link #onPause}, etc.) executing. * * <p><em>Derived classes must call through to the super class's * implementation of this method. If they do not, an exception will be diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 2d7ed46fe64a..f63170aa159d 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -104,6 +104,7 @@ import android.content.pm.ServiceInfo; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.content.res.ResourceTimer; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.content.res.loader.ResourcesLoader; @@ -5283,6 +5284,7 @@ public final class ActivityThread extends ClientTransactionHandler Resources.dumpHistory(pw, ""); pw.flush(); + ResourceTimer.dumpTimers(info.fd.getFileDescriptor(), "-refresh"); if (info.finishCallback != null) { info.finishCallback.sendResult(null); } diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index 3214bd8f01fc..2e8031dd22fe 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -840,7 +840,9 @@ public final class ApplicationStartInfo implements Parcelable { * @hide */ // LINT.IfChange(write_proto) - public void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException { + public void writeToProto(ProtoOutputStream proto, long fieldId, + ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream, + TypedXmlSerializer typedXmlSerializer) throws IOException { final long token = proto.start(fieldId); proto.write(ApplicationStartInfoProto.PID, mPid); proto.write(ApplicationStartInfoProto.REAL_UID, mRealUid); @@ -850,38 +852,38 @@ public final class ApplicationStartInfo implements Parcelable { proto.write(ApplicationStartInfoProto.STARTUP_STATE, mStartupState); proto.write(ApplicationStartInfoProto.REASON, mReason); if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) { - ByteArrayOutputStream timestampsBytes = new ByteArrayOutputStream(); - ObjectOutputStream timestampsOut = new ObjectOutputStream(timestampsBytes); - TypedXmlSerializer serializer = Xml.resolveSerializer(timestampsOut); - serializer.startDocument(null, true); - serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + byteArrayOutputStream = new ByteArrayOutputStream(); + objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + typedXmlSerializer = Xml.resolveSerializer(objectOutputStream); + typedXmlSerializer.startDocument(null, true); + typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); for (int i = 0; i < mStartupTimestampsNs.size(); i++) { - serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); - serializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY, + typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + typedXmlSerializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY, mStartupTimestampsNs.keyAt(i)); - serializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS, + typedXmlSerializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS, mStartupTimestampsNs.valueAt(i)); - serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); } - serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); - serializer.endDocument(); + typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + typedXmlSerializer.endDocument(); proto.write(ApplicationStartInfoProto.STARTUP_TIMESTAMPS, - timestampsBytes.toByteArray()); - timestampsOut.close(); + byteArrayOutputStream.toByteArray()); + objectOutputStream.close(); } proto.write(ApplicationStartInfoProto.START_TYPE, mStartType); if (mStartIntent != null) { - ByteArrayOutputStream intentBytes = new ByteArrayOutputStream(); - ObjectOutputStream intentOut = new ObjectOutputStream(intentBytes); - TypedXmlSerializer serializer = Xml.resolveSerializer(intentOut); - serializer.startDocument(null, true); - serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); - mStartIntent.saveToXml(serializer); - serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); - serializer.endDocument(); + byteArrayOutputStream = new ByteArrayOutputStream(); + objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + typedXmlSerializer = Xml.resolveSerializer(objectOutputStream); + typedXmlSerializer.startDocument(null, true); + typedXmlSerializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent.saveToXml(typedXmlSerializer); + typedXmlSerializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + typedXmlSerializer.endDocument(); proto.write(ApplicationStartInfoProto.START_INTENT, - intentBytes.toByteArray()); - intentOut.close(); + byteArrayOutputStream.toByteArray()); + objectOutputStream.close(); } proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode); proto.write(ApplicationStartInfoProto.WAS_FORCE_STOPPED, mWasForceStopped); @@ -900,7 +902,9 @@ public final class ApplicationStartInfo implements Parcelable { * @hide */ // LINT.IfChange(read_proto) - public void readFromProto(ProtoInputStream proto, long fieldId) + public void readFromProto(ProtoInputStream proto, long fieldId, + ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream, + TypedXmlPullParser typedXmlPullParser) throws IOException, WireTypeMismatchException, ClassNotFoundException { final long token = proto.start(fieldId); while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) { @@ -927,19 +931,21 @@ public final class ApplicationStartInfo implements Parcelable { mReason = proto.readInt(ApplicationStartInfoProto.REASON); break; case (int) ApplicationStartInfoProto.STARTUP_TIMESTAMPS: - ByteArrayInputStream timestampsBytes = new ByteArrayInputStream(proto.readBytes( + byteArrayInputStream = new ByteArrayInputStream(proto.readBytes( ApplicationStartInfoProto.STARTUP_TIMESTAMPS)); - ObjectInputStream timestampsIn = new ObjectInputStream(timestampsBytes); + objectInputStream = new ObjectInputStream(byteArrayInputStream); mStartupTimestampsNs = new ArrayMap<Integer, Long>(); try { - TypedXmlPullParser parser = Xml.resolvePullParser(timestampsIn); - XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); - int depth = parser.getDepth(); - while (XmlUtils.nextElementWithin(parser, depth)) { - if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(parser.getName())) { - int key = parser.getAttributeInt(null, + typedXmlPullParser = Xml.resolvePullParser(objectInputStream); + XmlUtils.beginDocument(typedXmlPullParser, + PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + int depth = typedXmlPullParser.getDepth(); + while (XmlUtils.nextElementWithin(typedXmlPullParser, depth)) { + if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals( + typedXmlPullParser.getName())) { + int key = typedXmlPullParser.getAttributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY); - long ts = parser.getAttributeLong(null, + long ts = typedXmlPullParser.getAttributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS); mStartupTimestampsNs.put(key, ts); } @@ -947,23 +953,24 @@ public final class ApplicationStartInfo implements Parcelable { } catch (XmlPullParserException e) { // Timestamps lost } - timestampsIn.close(); + objectInputStream.close(); break; case (int) ApplicationStartInfoProto.START_TYPE: mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE); break; case (int) ApplicationStartInfoProto.START_INTENT: - ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes( + byteArrayInputStream = new ByteArrayInputStream(proto.readBytes( ApplicationStartInfoProto.START_INTENT)); - ObjectInputStream intentIn = new ObjectInputStream(intentBytes); + objectInputStream = new ObjectInputStream(byteArrayInputStream); try { - TypedXmlPullParser parser = Xml.resolvePullParser(intentIn); - XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT); - mStartIntent = Intent.restoreFromXml(parser); + typedXmlPullParser = Xml.resolvePullParser(objectInputStream); + XmlUtils.beginDocument(typedXmlPullParser, + PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent = Intent.restoreFromXml(typedXmlPullParser); } catch (XmlPullParserException e) { // Intent lost } - intentIn.close(); + objectInputStream.close(); break; case (int) ApplicationStartInfoProto.LAUNCH_MODE: mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE); diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index 9d1d9c7b69de..fa977c93113a 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -19,7 +19,6 @@ package android.app; import static com.android.internal.util.Preconditions.checkArgument; import android.annotation.DrawableRes; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -52,48 +51,40 @@ public final class AutomaticZenRule implements Parcelable { * and the value returned if the true type was added in an API level higher than the calling * app's targetSdk. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_UNKNOWN = -1; /** * Rule is of a known type, but not one of the specific types. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_OTHER = 0; /** * The type for rules triggered according to a time-based schedule. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_SCHEDULE_TIME = 1; /** * The type for rules triggered by calendar events. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_SCHEDULE_CALENDAR = 2; /** * The type for rules triggered by bedtime/sleeping, like time of day, or snore detection. * * <p>Only the 'Wellbeing' app may own rules of this type. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_BEDTIME = 3; /** * The type for rules triggered by driving detection, like Bluetooth connections or vehicle * sounds. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_DRIVING = 4; /** * The type for rules triggered by the user entering an immersive activity, like opening an app * using {@link WindowInsetsController#hide(int)}. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_IMMERSIVE = 5; /** * The type for rules that have a {@link ZenPolicy} that implies that the * device should not make sound and potentially hide some visual effects; may be triggered * when entering a location where silence is requested, like a theater. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_THEATER = 6; /** * The type for rules created and managed by a device owner. These rules may not be fully @@ -101,7 +92,6 @@ public final class AutomaticZenRule implements Parcelable { * * <p>Only a 'Device Owner' app may own rules of this type. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int TYPE_MANAGED = 7; /** @hide */ @@ -127,17 +117,14 @@ public final class AutomaticZenRule implements Parcelable { /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_NAME = 1 << 0; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_INTERRUPTION_FILTER = 1 << 1; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_ICON = 1 << 2; private boolean enabled; @@ -149,10 +136,8 @@ public final class AutomaticZenRule implements Parcelable { private long creationTime; private ZenPolicy mZenPolicy; private ZenDeviceEffects mDeviceEffects; - // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined. - private boolean mModified = false; private String mPkg; - private int mType = Flags.modesApi() ? TYPE_UNKNOWN : 0; + private int mType = TYPE_UNKNOWN; private int mIconResId; private String mTriggerDescription; private boolean mAllowManualInvocation; @@ -229,8 +214,10 @@ public final class AutomaticZenRule implements Parcelable { /** * @hide + * @deprecated Do not add new usages; will be removed soon. */ - // TODO: b/310620812 - Remove when the flag is inlined (all system callers should use Builder). + // TODO: b/368247671 - Remove when modes_ui is inlined (remaining usages are in obsolete tests) + @Deprecated public AutomaticZenRule(String name, ComponentName owner, ComponentName configurationActivity, Uri conditionId, ZenPolicy policy, int interruptionFilter, boolean enabled, long creationTime) { @@ -251,15 +238,12 @@ public final class AutomaticZenRule implements Parcelable { source.readParcelable(null, android.content.ComponentName.class)); creationTime = source.readLong(); mZenPolicy = source.readParcelable(null, ZenPolicy.class); - mModified = source.readInt() == ENABLED; mPkg = source.readString(); - if (Flags.modesApi()) { - mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class); - mAllowManualInvocation = source.readBoolean(); - mIconResId = source.readInt(); - mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH); - mType = source.readInt(); - } + mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class); + mAllowManualInvocation = source.readBoolean(); + mIconResId = source.readInt(); + mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH); + mType = source.readInt(); } /** @@ -306,15 +290,6 @@ public final class AutomaticZenRule implements Parcelable { } /** - * Returns whether this rule's name has been modified by the user. - * @hide - */ - // TODO: b/310620812 - Consider removing completely. Seems not be used anywhere except tests. - public boolean isModified() { - return mModified; - } - - /** * Gets the {@link ZenPolicy} applied if {@link #getInterruptionFilter()} is * {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY}. */ @@ -325,7 +300,6 @@ public final class AutomaticZenRule implements Parcelable { /** Gets the {@link ZenDeviceEffects} of this rule. */ @Nullable - @FlaggedApi(Flags.FLAG_MODES_API) public ZenDeviceEffects getDeviceEffects() { return mDeviceEffects; } @@ -378,14 +352,6 @@ public final class AutomaticZenRule implements Parcelable { } /** - * Sets modified state of this rule. - * @hide - */ - public void setModified(boolean modified) { - this.mModified = modified; - } - - /** * Sets the {@link ZenPolicy} applied if {@link #getInterruptionFilter()} is * {@link NotificationManager#INTERRUPTION_FILTER_PRIORITY}. * @@ -404,7 +370,6 @@ public final class AutomaticZenRule implements Parcelable { * <p>When updating an existing rule via {@link NotificationManager#updateAutomaticZenRule}, * a {@code null} value here means the previous set of effects is retained. */ - @FlaggedApi(Flags.FLAG_MODES_API) public void setDeviceEffects(@Nullable ZenDeviceEffects deviceEffects) { mDeviceEffects = deviceEffects; } @@ -458,7 +423,6 @@ public final class AutomaticZenRule implements Parcelable { /** * Gets the type of the rule. */ - @FlaggedApi(Flags.FLAG_MODES_API) public @Type int getType() { return mType; } @@ -467,7 +431,6 @@ public final class AutomaticZenRule implements Parcelable { * Sets the type of the rule. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public void setType(@Type int type) { mType = checkValidType(type); } @@ -476,7 +439,6 @@ public final class AutomaticZenRule implements Parcelable { * Gets the user visible description of when this rule is active * (see {@link Condition#STATE_TRUE}). */ - @FlaggedApi(Flags.FLAG_MODES_API) public @Nullable String getTriggerDescription() { return mTriggerDescription; } @@ -489,7 +451,6 @@ public final class AutomaticZenRule implements Parcelable { * "When connected to [Car Name]". * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public void setTriggerDescription(@Nullable String triggerDescription) { mTriggerDescription = triggerDescription; } @@ -497,7 +458,6 @@ public final class AutomaticZenRule implements Parcelable { /** * Gets the resource id of the drawable icon for this rule. */ - @FlaggedApi(Flags.FLAG_MODES_API) public @DrawableRes int getIconResId() { return mIconResId; } @@ -506,7 +466,6 @@ public final class AutomaticZenRule implements Parcelable { * Sets a resource id of a tintable vector drawable representing the rule in image form. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public void setIconResId(int iconResId) { mIconResId = iconResId; } @@ -515,7 +474,6 @@ public final class AutomaticZenRule implements Parcelable { * Gets whether this rule can be manually activated by the user even when the triggering * condition for the rule is not met. */ - @FlaggedApi(Flags.FLAG_MODES_API) public boolean isManualInvocationAllowed() { return mAllowManualInvocation; } @@ -525,23 +483,18 @@ public final class AutomaticZenRule implements Parcelable { * condition for the rule is not met. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public void setManualInvocationAllowed(boolean allowManualInvocation) { mAllowManualInvocation = allowManualInvocation; } /** @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public void validate() { - if (Flags.modesApi()) { - checkValidType(mType); - if (mDeviceEffects != null) { - mDeviceEffects.validate(); - } + checkValidType(mType); + if (mDeviceEffects != null) { + mDeviceEffects.validate(); } } - @FlaggedApi(Flags.FLAG_MODES_API) @Type private static int checkValidType(@Type int type) { checkArgument(type >= TYPE_UNKNOWN && type <= TYPE_MANAGED, @@ -571,39 +524,34 @@ public final class AutomaticZenRule implements Parcelable { dest.writeParcelable(configurationActivity, 0); dest.writeLong(creationTime); dest.writeParcelable(mZenPolicy, 0); - dest.writeInt(mModified ? ENABLED : DISABLED); dest.writeString(mPkg); - if (Flags.modesApi()) { - dest.writeParcelable(mDeviceEffects, 0); - dest.writeBoolean(mAllowManualInvocation); - dest.writeInt(mIconResId); - dest.writeString(mTriggerDescription); - dest.writeInt(mType); - } + dest.writeParcelable(mDeviceEffects, 0); + dest.writeBoolean(mAllowManualInvocation); + dest.writeInt(mIconResId); + dest.writeString(mTriggerDescription); + dest.writeInt(mType); } @Override public String toString() { - StringBuilder sb = new StringBuilder(AutomaticZenRule.class.getSimpleName()).append('[') + return new StringBuilder(AutomaticZenRule.class.getSimpleName()) + .append('[') .append("enabled=").append(enabled) .append(",name=").append(name) + .append(",type=").append(mType) .append(",interruptionFilter=").append(interruptionFilter) .append(",pkg=").append(mPkg) .append(",conditionId=").append(conditionId) .append(",owner=").append(owner) .append(",configActivity=").append(configurationActivity) .append(",creationTime=").append(creationTime) - .append(",mZenPolicy=").append(mZenPolicy); - - if (Flags.modesApi()) { - sb.append(",deviceEffects=").append(mDeviceEffects) - .append(",allowManualInvocation=").append(mAllowManualInvocation) - .append(",iconResId=").append(mIconResId) - .append(",triggerDescription=").append(mTriggerDescription) - .append(",type=").append(mType); - } - - return sb.append(']').toString(); + .append(",mZenPolicy=").append(mZenPolicy) + .append(",deviceEffects=").append(mDeviceEffects) + .append(",allowManualInvocation=").append(mAllowManualInvocation) + .append(",iconResId=").append(mIconResId) + .append(",triggerDescription=").append(mTriggerDescription) + .append(']') + .toString(); } /** @hide */ @@ -626,8 +574,7 @@ public final class AutomaticZenRule implements Parcelable { if (!(o instanceof AutomaticZenRule)) return false; if (o == this) return true; final AutomaticZenRule other = (AutomaticZenRule) o; - boolean finalEquals = other.enabled == enabled - && other.mModified == mModified + return other.enabled == enabled && Objects.equals(other.name, name) && other.interruptionFilter == interruptionFilter && Objects.equals(other.conditionId, conditionId) @@ -635,27 +582,19 @@ public final class AutomaticZenRule implements Parcelable { && Objects.equals(other.mZenPolicy, mZenPolicy) && Objects.equals(other.configurationActivity, configurationActivity) && Objects.equals(other.mPkg, mPkg) - && other.creationTime == creationTime; - if (Flags.modesApi()) { - return finalEquals - && Objects.equals(other.mDeviceEffects, mDeviceEffects) - && other.mAllowManualInvocation == mAllowManualInvocation - && other.mIconResId == mIconResId - && Objects.equals(other.mTriggerDescription, mTriggerDescription) - && other.mType == mType; - } - return finalEquals; + && other.creationTime == creationTime + && Objects.equals(other.mDeviceEffects, mDeviceEffects) + && other.mAllowManualInvocation == mAllowManualInvocation + && other.mIconResId == mIconResId + && Objects.equals(other.mTriggerDescription, mTriggerDescription) + && other.mType == mType; } @Override public int hashCode() { - if (Flags.modesApi()) { - return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, - configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime, - mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType); - } return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, - configurationActivity, mZenPolicy, mModified, creationTime, mPkg); + configurationActivity, mZenPolicy, mDeviceEffects, creationTime, + mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType); } public static final @android.annotation.NonNull Parcelable.Creator<AutomaticZenRule> CREATOR @@ -705,7 +644,6 @@ public final class AutomaticZenRule implements Parcelable { return input; } - @FlaggedApi(Flags.FLAG_MODES_API) public static final class Builder { private String mName; private ComponentName mOwner; diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index b9255ecaf1b6..00df7246a300 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -225,8 +225,6 @@ interface INotificationManager ZenPolicy getDefaultZenPolicy(); AutomaticZenRule getAutomaticZenRule(String id); Map<String, AutomaticZenRule> getAutomaticZenRules(); - // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined. - List<ZenModeConfig.ZenRule> getZenRules(); String addAutomaticZenRule(in AutomaticZenRule automaticZenRule, String pkg, boolean fromUser); boolean updateAutomaticZenRule(String id, in AutomaticZenRule automaticZenRule, boolean fromUser); boolean removeAutomaticZenRule(String id, boolean fromUser); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 0ca4a329fd5a..5dca1c70a2e6 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -6002,7 +6002,7 @@ public class Notification implements Parcelable // HUNS, which use a different layout that already accounts for that). Templates that // have content that will be displayed under the small icon also use a different margin. if (Flags.notificationsRedesignTemplates() - && !p.mHeaderless && !p.mHasContentInLeftMargin) { + && !p.mHeaderless && !p.mSkipTopLineAlignment) { int margin = getContentMarginTop(mContext, R.dimen.notification_2025_content_margin_top); contentView.setViewLayoutMargin(R.id.notification_main_column, @@ -6594,13 +6594,8 @@ public class Notification implements Parcelable int notifMargin = resources.getDimensionPixelSize(R.dimen.notification_2025_margin); // Spacing between the text lines, scaling with the font size (originally in sp) int spacing = resources.getDimensionPixelSize(spacingRes); - // Size of the text in the notification top line (originally in sp) - int[] textSizeAttr = new int[] { android.R.attr.textSize }; - TypedArray typedArray = context.obtainStyledAttributes( - R.style.TextAppearance_DeviceDefault_Notification_Info, textSizeAttr); - int textSize = typedArray.getDimensionPixelSize(0 /* index */, -1 /* default */); - typedArray.recycle(); + int textSize = resources.getDimensionPixelSize(R.dimen.notification_subtext_size); // Adding up all the values as pixels return notifMargin + spacing + textSize; @@ -9503,7 +9498,7 @@ public class Notification implements Parcelable .hideLeftIcon(isOneToOne) .hideRightIcon(hideRightIcons || isOneToOne) .headerTextSecondary(isHeaderless ? null : conversationTitle) - .hasContentInLeftMargin(true); + .skipTopLineAlignment(true); RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( isConversationLayout ? mBuilder.getConversationLayoutResource() @@ -14681,7 +14676,7 @@ public class Notification implements Parcelable Icon mPromotedPicture; boolean mCallStyleActions; boolean mAllowTextWithProgress; - boolean mHasContentInLeftMargin; + boolean mSkipTopLineAlignment; int mTitleViewId; int mTextViewId; @Nullable CharSequence mTitle; @@ -14707,7 +14702,7 @@ public class Notification implements Parcelable mPromotedPicture = null; mCallStyleActions = false; mAllowTextWithProgress = false; - mHasContentInLeftMargin = false; + mSkipTopLineAlignment = false; mTitleViewId = R.id.title; mTextViewId = R.id.text; mTitle = null; @@ -14774,8 +14769,8 @@ public class Notification implements Parcelable return this; } - public StandardTemplateParams hasContentInLeftMargin(boolean hasContentInLeftMargin) { - mHasContentInLeftMargin = hasContentInLeftMargin; + public StandardTemplateParams skipTopLineAlignment(boolean skipTopLineAlignment) { + mSkipTopLineAlignment = skipTopLineAlignment; return this; } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 00f896deae4b..726999a08322 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -356,7 +356,6 @@ public class NotificationManager { * a DND component, the rule owner should activate any extra behavior that's part of that mode * in response to this broadcast. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int AUTOMATIC_RULE_STATUS_ACTIVATED = 4; /** @@ -367,7 +366,6 @@ public class NotificationManager { * longer met) and then {@link Condition#STATE_TRUE} when the trigger criteria is freshly met, * or when the user re-activates it. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int AUTOMATIC_RULE_STATUS_DEACTIVATED = 5; /** @@ -415,7 +413,6 @@ public class NotificationManager { * <p>This broadcast is only sent to registered receivers and receivers in packages that have * been granted Notification Policy access (see {@link #isNotificationPolicyAccessGranted()}). */ - @FlaggedApi(Flags.FLAG_MODES_API) @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED = "android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED"; @@ -425,7 +422,6 @@ public class NotificationManager { * {@link #ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED} containing the new * {@link Policy} value. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final String EXTRA_NOTIFICATION_POLICY = "android.app.extra.NOTIFICATION_POLICY"; @@ -1726,9 +1722,8 @@ public class NotificationManager { * rule management to system settings/uis via * {@link Settings#ACTION_AUTOMATIC_ZEN_RULE_SETTINGS}. */ - @FlaggedApi(Flags.FLAG_MODES_API) public boolean areAutomaticZenRulesUserManaged() { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { PackageManager pm = mContext.getPackageManager(); return !pm.hasSystemFeature(PackageManager.FEATURE_WATCH) && !pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) @@ -1748,21 +1743,7 @@ public class NotificationManager { public Map<String, AutomaticZenRule> getAutomaticZenRules() { INotificationManager service = service(); try { - if (Flags.modesApi()) { - return service.getAutomaticZenRules(); - } else { - List<ZenModeConfig.ZenRule> rules = service.getZenRules(); - Map<String, AutomaticZenRule> ruleMap = new HashMap<>(); - for (ZenModeConfig.ZenRule rule : rules) { - AutomaticZenRule azr = new AutomaticZenRule(rule.name, rule.component, - rule.configurationActivity, rule.conditionId, rule.zenPolicy, - zenModeToInterruptionFilter(rule.zenMode), rule.enabled, - rule.creationTime); - azr.setPackageName(rule.pkg); - ruleMap.put(rule.id, azr); - } - return ruleMap; - } + return service.getAutomaticZenRules(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1804,7 +1785,6 @@ public class NotificationManager { /** @hide */ @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) @NonNull public String addAutomaticZenRule(@NonNull AutomaticZenRule automaticZenRule, boolean fromUser) { @@ -1840,7 +1820,6 @@ public class NotificationManager { /** @hide */ @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) public boolean updateAutomaticZenRule(@NonNull String id, @NonNull AutomaticZenRule automaticZenRule, boolean fromUser) { INotificationManager service = service(); @@ -1860,7 +1839,6 @@ public class NotificationManager { * @param id The id of the rule * @return the state of the rule. */ - @FlaggedApi(Flags.FLAG_MODES_API) @Condition.State public int getAutomaticZenRuleState(@NonNull String id) { INotificationManager service = service(); @@ -1935,7 +1913,6 @@ public class NotificationManager { /** @hide */ @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) public boolean removeAutomaticZenRule(@NonNull String id, boolean fromUser) { INotificationManager service = service(); try { @@ -2326,7 +2303,6 @@ public class NotificationManager { * @hide */ @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) public @NonNull ZenPolicy getDefaultZenPolicy() { INotificationManager service = service(); try { @@ -2693,7 +2669,7 @@ public class NotificationManager { /** * @hide */ - public static final int STATE_CHANNELS_BYPASSING_DND = 1 << 0; + public static final int STATE_HAS_PRIORITY_CHANNELS = 1 << 0; /** * Whether the policy indicates that even priority channels are NOT permitted to bypass DND. @@ -2918,7 +2894,7 @@ public class NotificationManager { @Override public String toString() { - StringBuilder sb = new StringBuilder().append("NotificationManager.Policy[") + return new StringBuilder().append("NotificationManager.Policy[") .append("priorityCategories=") .append(priorityCategoriesToString(priorityCategories)) .append(",priorityCallSenders=") @@ -2928,24 +2904,19 @@ public class NotificationManager { .append(",priorityConvSenders=") .append(conversationSendersToString(priorityConversationSenders)) .append(",suppressedVisualEffects=") - .append(suppressedEffectsToString(suppressedVisualEffects)); - if (Flags.modesApi()) { - sb.append(",hasPriorityChannels="); - } else { - sb.append(",areChannelsBypassingDnd="); - } - sb.append((state == STATE_UNSET - ? "unset" - : ((state & STATE_CHANNELS_BYPASSING_DND) != 0) - ? "true" - : "false")); - if (Flags.modesApi()) { - sb.append(",allowPriorityChannels=") - .append((state == STATE_UNSET - ? "unset" - : (allowPriorityChannels() ? "true" : "false"))); - } - return sb.append("]").toString(); + .append(suppressedEffectsToString(suppressedVisualEffects)) + .append(",hasPriorityChannels=") + .append((state == STATE_UNSET + ? "unset" + : ((state & STATE_HAS_PRIORITY_CHANNELS) != 0) + ? "true" + : "false")) + .append(",allowPriorityChannels=") + .append((state == STATE_UNSET + ? "unset" + : (allowPriorityChannels() ? "true" : "false"))) + .append("]") + .toString(); } /** @hide */ @@ -3220,7 +3191,6 @@ public class NotificationManager { } /** @hide **/ - @FlaggedApi(Flags.FLAG_MODES_API) @TestApi // so CTS tests can read this state without having to use implementation detail public boolean allowPriorityChannels() { if (state == STATE_UNSET) { @@ -3230,17 +3200,15 @@ public class NotificationManager { } /** @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public boolean hasPriorityChannels() { - return (state & STATE_CHANNELS_BYPASSING_DND) != 0; + return (state & STATE_HAS_PRIORITY_CHANNELS) != 0; } /** @hide **/ - @FlaggedApi(Flags.FLAG_MODES_API) public static int policyState(boolean hasPriorityChannels, boolean allowPriorityChannels) { int state = 0; if (hasPriorityChannels) { - state |= STATE_CHANNELS_BYPASSING_DND; + state |= STATE_HAS_PRIORITY_CHANNELS; } if (!allowPriorityChannels) { state |= STATE_PRIORITY_CHANNELS_BLOCKED; diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 7b63ab80964d..464bcc025d92 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -956,10 +956,9 @@ public final class UiAutomation { * <p> * <strong>Note:</strong> It is caller's responsibility to recycle the event. * </p> - * - * @param event The event to inject. - * @param sync Whether to inject the event synchronously. - * @return Whether event injection succeeded. + * @param event the event to inject + * @param sync whether to inject the event synchronously + * @return {@code true} if event injection succeeded */ public boolean injectInputEvent(InputEvent event, boolean sync) { return injectInputEvent(event, sync, true /* waitForAnimations */); @@ -972,15 +971,21 @@ public final class UiAutomation { * <strong>Note:</strong> It is caller's responsibility to recycle the event. * </p> * - * @param event The event to inject. - * @param sync Whether to inject the event synchronously. - * @param waitForAnimations Whether to wait for all window container animations and surface - * operations to complete. - * @return Whether event injection succeeded. + * @param event the event to inject + * @param sync whether to inject the event synchronously. + * @param waitForAnimations whether to wait for all window container animations and surface + * operations to complete + * @return {@code true} if event injection succeeded * + * @deprecated for CTS tests prefer inject input events using uinput + * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice). + * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used + * for in-process injection. * @hide */ @TestApi + @Deprecated // Deprecated for CTS tests + @SuppressLint("UnflaggedApi") // @FlaggedApi breaks previously released @TestApi, b/395889250 public boolean injectInputEvent(@NonNull InputEvent event, boolean sync, boolean waitForAnimations) { try { @@ -1003,9 +1008,15 @@ public final class UiAutomation { * Events injected to the input subsystem using the standard {@link #injectInputEvent} method * skip the accessibility input filter to avoid feedback loops. * + * @deprecated for CTS tests prefer inject input events using uinput + * (com.android.cts.input.UinputDevice) or hid devices (com.android.cts.input.HidDevice). + * Alternatively, InjectInputInProcess (com.android.cts.input.InjectInputProcess) can be used + * for in-process injection. * @hide */ @TestApi + @Deprecated + @SuppressLint("UnflaggedApi") // @FlaggedApi breaks previously released @TestApi, b/395889250 public void injectInputEventToInputFilter(@NonNull InputEvent event) { try { mUiAutomationConnection.injectInputEventToInputFilter(event); diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index f6c789d51aee..33466dd79be1 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -312,7 +312,6 @@ public class UiModeManager { * #getAttentionModeThemeOverlay()}: Keeps night mode as set by {@link #setNightMode(int)}. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) @TestApi public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; @@ -321,7 +320,6 @@ public class UiModeManager { * #getAttentionModeThemeOverlay()}: Maintains night mode always on. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) @TestApi public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; @@ -330,7 +328,6 @@ public class UiModeManager { * #getAttentionModeThemeOverlay()}: Maintains night mode always off (Light). * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) @TestApi public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; @@ -338,7 +335,6 @@ public class UiModeManager { * Constant for {@link #getAttentionModeThemeOverlay()}: Error communication with server. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) @TestApi public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; @@ -940,7 +936,6 @@ public class UiModeManager { * {@code AttentionModeThemeOverlayType}. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public void setAttentionModeThemeOverlay( @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) { @@ -967,7 +962,6 @@ public class UiModeManager { * * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) @TestApi @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public @AttentionModeThemeOverlayReturnType int getAttentionModeThemeOverlay() { diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java index 2ce431dcb32d..4e5fa6bac951 100644 --- a/core/java/android/app/contextualsearch/ContextualSearchManager.java +++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java @@ -32,6 +32,9 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; /** * {@link ContextualSearchManager} is a system service to facilitate contextual search experience on @@ -39,10 +42,8 @@ import java.lang.annotation.RetentionPolicy; * <p> * This class lets a caller start contextual search by calling {@link #startContextualSearch} * method. - * - * @hide */ -@SystemApi +@FlaggedApi(Flags.FLAG_SELF_INVOCATION) public final class ContextualSearchManager { /** @@ -50,7 +51,9 @@ public final class ContextualSearchManager { * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. * * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH + * @hide */ + @SystemApi public static final String EXTRA_ENTRYPOINT = "android.app.contextualsearch.extra.ENTRYPOINT"; @@ -60,7 +63,9 @@ public final class ContextualSearchManager { * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. * * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH + * @hide */ + @SystemApi public static final String EXTRA_FLAG_SECURE_FOUND = "android.app.contextualsearch.extra.FLAG_SECURE_FOUND"; @@ -69,7 +74,9 @@ public final class ContextualSearchManager { * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. * * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH + * @hide */ + @SystemApi public static final String EXTRA_SCREENSHOT = "android.app.contextualsearch.extra.SCREENSHOT"; @@ -79,7 +86,9 @@ public final class ContextualSearchManager { * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. * * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH + * @hide */ + @SystemApi public static final String EXTRA_IS_MANAGED_PROFILE_VISIBLE = "android.app.contextualsearch.extra.IS_MANAGED_PROFILE_VISIBLE"; @@ -89,7 +98,9 @@ public final class ContextualSearchManager { * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. * * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH + * @hide */ + @SystemApi public static final String EXTRA_VISIBLE_PACKAGE_NAMES = "android.app.contextualsearch.extra.VISIBLE_PACKAGE_NAMES"; @@ -98,10 +109,9 @@ public final class ContextualSearchManager { * {@link SystemClock#uptimeMillis()}. * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. * - * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH - * * TODO: un-hide in W * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH * @hide */ public static final String EXTRA_INVOCATION_TIME_MS = @@ -113,7 +123,9 @@ public final class ContextualSearchManager { * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. * * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH + * @hide */ + @SystemApi public static final String EXTRA_TOKEN = "android.app.contextualsearch.extra.TOKEN"; /** @@ -132,7 +144,10 @@ public final class ContextualSearchManager { * experience must add this intent filter action to the activity it wants to be launched. * <br> * <b>Note</b> This activity must not be exported. + * + * @hide */ + @SystemApi public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH = "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH"; @@ -144,23 +159,63 @@ public final class ContextualSearchManager { public static final String FEATURE_CONTEXTUAL_SEARCH = "com.google.android.feature.CONTEXTUAL_SEARCH"; - /** Entrypoint to be used when a user long presses on the nav handle. */ + /** + * Entrypoint to be used when a user long presses on the nav handle. + * + * @hide + */ + @SystemApi public static final int ENTRYPOINT_LONG_PRESS_NAV_HANDLE = 1; - /** Entrypoint to be used when a user long presses on the home button. */ + + /** Entrypoint to be used when a user long presses on the home button. + * + * @hide + */ + @SystemApi public static final int ENTRYPOINT_LONG_PRESS_HOME = 2; - /** Entrypoint to be used when a user long presses on the overview button. */ + + /** Entrypoint to be used when a user long presses on the overview button. + * + * @hide + */ + @SystemApi public static final int ENTRYPOINT_LONG_PRESS_OVERVIEW = 3; - /** Entrypoint to be used when a user presses the action button in overview. */ + + /** + * Entrypoint to be used when a user presses the action button in overview. + * + * @hide + */ + @SystemApi public static final int ENTRYPOINT_OVERVIEW_ACTION = 4; - /** Entrypoint to be used when a user presses the context menu button in overview. */ + + /** + * Entrypoint to be used when a user presses the context menu button in overview. + * + * @hide + */ + @SystemApi public static final int ENTRYPOINT_OVERVIEW_MENU = 5; - /** Entrypoint to be used by system actions like TalkBack, Accessibility etc. */ + + /** + * Entrypoint to be used by system actions like TalkBack, Accessibility etc. + * + * @hide + */ + @SystemApi public static final int ENTRYPOINT_SYSTEM_ACTION = 9; - /** Entrypoint to be used when a user long presses on the meta key. */ + + /** + * Entrypoint to be used when a user long presses on the meta key. + * + * @hide + */ + @SystemApi public static final int ENTRYPOINT_LONG_PRESS_META = 10; + /** * The {@link Entrypoint} annotation is used to standardize the entrypoints supported by - * {@link #startContextualSearch} method. + * {@link #startContextualSearch(int entrypoint)} method. * * @hide */ @@ -174,8 +229,18 @@ public final class ContextualSearchManager { ENTRYPOINT_LONG_PRESS_META }) @Retention(RetentionPolicy.SOURCE) - public @interface Entrypoint { - } + public @interface Entrypoint {} + + private static final Set<Integer> VALID_ENTRYPOINT_VALUES = new HashSet<>(Arrays.asList( + ENTRYPOINT_LONG_PRESS_NAV_HANDLE, + ENTRYPOINT_LONG_PRESS_HOME, + ENTRYPOINT_LONG_PRESS_OVERVIEW, + ENTRYPOINT_OVERVIEW_ACTION, + ENTRYPOINT_OVERVIEW_MENU, + ENTRYPOINT_SYSTEM_ACTION, + ENTRYPOINT_LONG_PRESS_META + )); + private static final String TAG = ContextualSearchManager.class.getSimpleName(); private static final boolean DEBUG = false; @@ -189,7 +254,7 @@ public final class ContextualSearchManager { } /** - * Used to start contextual search. + * Used to start contextual search for a given system entrypoint. * <p> * When {@link #startContextualSearch} is called, the system server does the following: * <ul> @@ -202,9 +267,15 @@ public final class ContextualSearchManager { * </p> * * @param entrypoint the invocation entrypoint + * + * @hide */ @RequiresPermission(ACCESS_CONTEXTUAL_SEARCH) + @SystemApi public void startContextualSearch(@Entrypoint int entrypoint) { + if (!VALID_ENTRYPOINT_VALUES.contains(entrypoint)) { + throw new IllegalArgumentException("Invalid entrypoint: " + entrypoint); + } if (DEBUG) Log.d(TAG, "startContextualSearch for entrypoint: " + entrypoint); try { mService.startContextualSearch(entrypoint); @@ -213,4 +284,22 @@ public final class ContextualSearchManager { e.rethrowFromSystemServer(); } } + + /** + * Used to start contextual search from within an app. + * + * <p>System apps should use the available System APIs rather than this method. + * + * @throws SecurityException if the caller does not have a foreground Activity. + */ + @FlaggedApi(Flags.FLAG_SELF_INVOCATION) + public void startContextualSearch() { + if (DEBUG) Log.d(TAG, "startContextualSearch from app"); + try { + mService.startContextualSearchForForegroundApp(); + } catch (RemoteException e) { + if (DEBUG) Log.d(TAG, "Failed to startContextualSearch", e); + e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/contextualsearch/IContextualSearchManager.aidl b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl index 9b0b8b775971..8789daab3afe 100644 --- a/core/java/android/app/contextualsearch/IContextualSearchManager.aidl +++ b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl @@ -4,7 +4,8 @@ import android.app.contextualsearch.IContextualSearchCallback; /** * @hide */ -oneway interface IContextualSearchManager { - void startContextualSearch(int entrypoint); - void getContextualSearchState(in IBinder token, in IContextualSearchCallback callback); +interface IContextualSearchManager { + void startContextualSearchForForegroundApp(); + oneway void startContextualSearch(int entrypoint); + oneway void getContextualSearchState(in IBinder token, in IContextualSearchCallback callback); } diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig index d81ec1e8b883..bc1f7cea7fce 100644 --- a/core/java/android/app/contextualsearch/flags.aconfig +++ b/core/java/android/app/contextualsearch/flags.aconfig @@ -39,3 +39,11 @@ flag { description: "Add audio playing status to the contextual search invocation intent." bug: "372935419" } + +flag { + name: "self_invocation" + namespace: "sysui_integrations" + description: "Enable apps to self-invoke Contextual Search." + bug: "368653769" + is_exported: true +} diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java index a04f96a9f6e3..9c85b09f6be3 100644 --- a/core/java/android/app/jank/JankTracker.java +++ b/core/java/android/app/jank/JankTracker.java @@ -20,6 +20,7 @@ import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.os.Handler; import android.os.HandlerThread; +import android.util.Log; import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.SurfaceControl; @@ -30,16 +31,22 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** * This class is responsible for registering callbacks that will receive JankData batches. * It handles managing the background thread that JankData will be processed on. As well as acting * as an intermediary between widgets and the state tracker, routing state changes to the tracker. + * * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public class JankTracker { - + private static final boolean DEBUG = false; + private static final String DEBUG_KEY = "JANKTRACKER"; + // How long to delay the JankData listener registration. + //TODO b/394956095 see if this can be reduced or eliminated. + private static final int REGISTRATION_DELAY_MS = 1000; // Tracks states reported by widgets. private StateTracker mStateTracker; // Processes JankData batches and associates frames to widget states. @@ -49,9 +56,6 @@ public class JankTracker { private HandlerThread mHandlerThread = new HandlerThread("AppJankTracker"); private Handler mHandler = null; - // Needed so we know when the view is attached to a window. - private ViewTreeObserver mViewTreeObserver; - // Handle to a registered OnJankData listener. private SurfaceControl.OnJankDataListenerRegistration mJankDataListenerRegistration; @@ -76,6 +80,40 @@ public class JankTracker { */ private boolean mListenersRegistered = false; + @FlaggedApi(com.android.window.flags.Flags.FLAG_JANK_API) + private final SurfaceControl.OnJankDataListener mJankDataListener = + new SurfaceControl.OnJankDataListener() { + @Override + public void onJankDataAvailable( + @androidx.annotation.NonNull List<SurfaceControl.JankData> jankData) { + if (mJankDataProcessor == null) return; + mJankDataProcessor.processJankData(jankData, mActivityName, mAppUid); + } + }; + + private final ViewTreeObserver.OnWindowAttachListener mOnWindowAttachListener = + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + getHandler().postDelayed(new Runnable() { + @Override + public void run() { + mDecorView.getViewTreeObserver() + .removeOnWindowAttachListener(mOnWindowAttachListener); + registerForJankData(); + } + }, REGISTRATION_DELAY_MS); + } + + // Leave this empty. Only need to know when the DecorView is attached to the Window + // in order to get a handle to AttachedSurfaceControl. There is no need to tie + // anything to when the view is detached as all un-registration code is tied to + // the lifecycle of the enclosing activity. + @Override + public void onWindowDetached() { + + } + }; public JankTracker(Choreographer choreographer, View decorView) { mStateTracker = new StateTracker(choreographer); @@ -108,9 +146,10 @@ public class JankTracker { /** * Will add the widget category, id and state as a UI state to associate frames to it. + * * @param widgetCategory preselected general widget category - * @param widgetId developer defined widget id if available. - * @param widgetState the current active widget state. + * @param widgetId developer defined widget id if available. + * @param widgetState the current active widget state. */ public void addUiState(String widgetCategory, String widgetId, String widgetState) { if (!shouldTrack()) return; @@ -121,9 +160,10 @@ public class JankTracker { /** * Will remove the widget category, id and state as a ui state and no longer attribute frames * to it. + * * @param widgetCategory preselected general widget category - * @param widgetId developer defined widget id if available. - * @param widgetState no longer active widget state. + * @param widgetId developer defined widget id if available. + * @param widgetState no longer active widget state. */ public void removeUiState(String widgetCategory, String widgetId, String widgetState) { if (!shouldTrack()) return; @@ -133,10 +173,11 @@ public class JankTracker { /** * Call to update a jank state to a different state. + * * @param widgetCategory preselected general widget category. - * @param widgetId developer defined widget id if available. - * @param currentState current state of the widget. - * @param nextState the state the widget will be in. + * @param widgetId developer defined widget id if available. + * @param currentState current state of the widget. + * @param nextState the state the widget will be in. */ public void updateUiState(String widgetCategory, String widgetId, String currentState, String nextState) { @@ -150,10 +191,11 @@ public class JankTracker { */ public void enableAppJankTracking() { // Add the activity as a state, this will ensure we track frames to the activity without the - // need of a decorated widget to be used. + // need for a decorated widget to be used. // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. mStateTracker.putState("NONE", mActivityName, "NONE"); mTrackingEnabled = true; + registerForJankData(); } /** @@ -163,10 +205,12 @@ public class JankTracker { mTrackingEnabled = false; // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. mStateTracker.removeState("NONE", mActivityName, "NONE"); + unregisterForJankData(); } /** * Retrieve all pending widget states, this is intended for testing purposes only. + * * @param stateDataList the ArrayList that will be populated with the pending states. */ @VisibleForTesting @@ -190,16 +234,35 @@ public class JankTracker { @VisibleForTesting public void forceListenerRegistration() { mSurfaceControl = mDecorView.getRootSurfaceControl(); - registerForJankData(); - // TODO b/376116199 Check if registration is good. - mListenersRegistered = true; + registerJankDataListener(); + } + + private void unregisterForJankData() { + if (mJankDataListenerRegistration == null) return; + + if (com.android.window.flags.Flags.jankApi()) { + mJankDataListenerRegistration.release(); + } + mJankDataListenerRegistration = null; + mListenersRegistered = false; } private void registerForJankData() { - if (mSurfaceControl == null) return; - /* - TODO b/376115668 Register for JankData batches from new JankTracking API - */ + if (mDecorView == null) return; + + mSurfaceControl = mDecorView.getRootSurfaceControl(); + + if (mSurfaceControl == null || mListenersRegistered) return; + + // Wait a short time before registering the listener. During development it was observed + // that if a listener is registered too quickly after a hot or warm start no data is + // received b/394956095. + getHandler().postDelayed(new Runnable() { + @Override + public void run() { + registerJankDataListener(); + } + }, REGISTRATION_DELAY_MS); } /** @@ -218,23 +281,30 @@ public class JankTracker { */ private void registerWindowListeners() { if (mDecorView == null) return; - mViewTreeObserver = mDecorView.getViewTreeObserver(); - mViewTreeObserver.addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - getHandler().postDelayed(new Runnable() { - @Override - public void run() { - forceListenerRegistration(); - } - }, 1000); + mDecorView.getViewTreeObserver().addOnWindowAttachListener(mOnWindowAttachListener); + } + + private void registerJankDataListener() { + if (mSurfaceControl == null) { + if (DEBUG) { + Log.d(DEBUG_KEY, "SurfaceControl is Null"); } + return; + } - @Override - public void onWindowDetached() { - // TODO b/376116199 do we un-register the callback or just not process the data. + if (com.android.window.flags.Flags.jankApi()) { + mJankDataListenerRegistration = mSurfaceControl.registerOnJankDataListener( + mHandlerThread.getThreadExecutor(), mJankDataListener); + + if (mJankDataListenerRegistration + == SurfaceControl.OnJankDataListenerRegistration.NONE) { + if (DEBUG) { + Log.d(DEBUG_KEY, "OnJankDataListenerRegistration is assigned NONE"); + } + return; } - }); + mListenersRegistered = true; + } } private Handler getHandler() { diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 9d8ab03982e6..8e6b88c66408 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -26,6 +26,7 @@ flag { bug: "378660052" } +# Flag for finalized API: In Nextfood but exported (and therefore must stay). flag { name: "modes_api" is_exported: true @@ -82,6 +83,13 @@ flag { } flag { + name: "modes_cleanup_implicit" + namespace: "systemui" + description: "Deletes implicit modes if never customized and not used for some time. Depends on MODES_UI" + bug: "394087495" +} + +flag { name: "api_tvextender" is_exported: true namespace: "systemui" @@ -321,7 +329,7 @@ flag { name: "no_sbnholder" namespace: "systemui" description: "removes sbnholder from NLS" - bug: "362981561" + bug: "378128805" } flag { diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl index e583302e4d3b..2f67a8abcd17 100644 --- a/core/java/android/app/supervision/ISupervisionManager.aidl +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -16,11 +16,14 @@ package android.app.supervision; +import android.content.Intent; + /** * Internal IPC interface to the supervision service. * {@hide} */ interface ISupervisionManager { + Intent createConfirmSupervisionCredentialsIntent(); boolean isSupervisionEnabledForUser(int userId); void setSupervisionEnabledForUser(int userId, boolean enabled); String getActiveSupervisionAppPackage(int userId); diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index d30705536045..0270edf080a9 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -16,13 +16,22 @@ package android.app.supervision; +import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_USERS; +import static android.Manifest.permission.QUERY_USERS; + +import android.annotation.FlaggedApi; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.annotation.UserIdInt; +import android.app.supervision.flags.Flags; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.content.Intent; import android.os.RemoteException; /** @@ -31,6 +40,8 @@ import android.os.RemoteException; * @hide */ @SystemService(Context.SUPERVISION_SERVICE) +@SystemApi +@FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS) public class SupervisionManager { private final Context mContext; @Nullable private final ISupervisionManager mService; @@ -47,7 +58,8 @@ public class SupervisionManager { * * @hide */ - public static final String ACTION_ENABLE_SUPERVISION = "android.app.action.ENABLE_SUPERVISION"; + public static final String ACTION_ENABLE_SUPERVISION = + "android.app.supervision.action.ENABLE_SUPERVISION"; /** * Activity action: ask the human user to disable supervision for this user. Only the app that @@ -62,7 +74,7 @@ public class SupervisionManager { * @hide */ public static final String ACTION_DISABLE_SUPERVISION = - "android.app.action.DISABLE_SUPERVISION"; + "android.app.supervision.action.DISABLE_SUPERVISION"; /** @hide */ @UnsupportedAppUsage @@ -72,11 +84,46 @@ public class SupervisionManager { } /** + * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to + * launch the activity to verify supervision credentials. + * + * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this API + * is called, the launched activity still need to perform validity checks as the supervision + * state can change when the activity is launched. A null intent is returned if supervision is + * disabled at the time of this API call. + * + * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification + * of the supervision credentials. + * + * @hide + */ + @RequiresPermission(value = android.Manifest.permission.QUERY_USERS) + @Nullable + public Intent createConfirmSupervisionCredentialsIntent() { + if (mService != null) { + try { + Intent result = mService.createConfirmSupervisionCredentialsIntent(); + if (result != null) { + result.prepareToEnterProcess( + Intent.LOCAL_FLAG_FROM_SYSTEM, mContext.getAttributionSource()); + } + return result; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return null; + } + + /** * Returns whether the device is supervised. * * @hide */ - @UserHandleAware + @SystemApi + @FlaggedApi(Flags.FLAG_SUPERVISION_MANAGER_APIS) + @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS}) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public boolean isSupervisionEnabled() { return isSupervisionEnabledForUser(mContext.getUserId()); } @@ -84,14 +131,10 @@ public class SupervisionManager { /** * Returns whether the device is supervised. * - * <p>The caller must be from the same user as the target or hold the {@link - * android.Manifest.permission#INTERACT_ACROSS_USERS} permission. - * * @hide */ - @RequiresPermission( - value = android.Manifest.permission.INTERACT_ACROSS_USERS, - conditional = true) + @RequiresPermission(anyOf = {MANAGE_USERS, QUERY_USERS}) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { if (mService != null) { try { @@ -108,7 +151,8 @@ public class SupervisionManager { * * @hide */ - @UserHandleAware + @TestApi + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public void setSupervisionEnabled(boolean enabled) { setSupervisionEnabledForUser(mContext.getUserId(), enabled); } @@ -116,14 +160,9 @@ public class SupervisionManager { /** * Sets whether the device is supervised for a given user. * - * <p>The caller must be from the same user as the target or hold the {@link - * android.Manifest.permission#INTERACT_ACROSS_USERS} permission. - * * @hide */ - @RequiresPermission( - value = android.Manifest.permission.INTERACT_ACROSS_USERS, - conditional = true) + @UserHandleAware(requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS) public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { if (mService != null) { try { diff --git a/core/java/android/app/supervision/flags.aconfig b/core/java/android/app/supervision/flags.aconfig index 232883cbfe00..94de03877fd7 100644 --- a/core/java/android/app/supervision/flags.aconfig +++ b/core/java/android/app/supervision/flags.aconfig @@ -64,3 +64,11 @@ flag { description: "Flag that enables the Supervision pin recovery screen with Supervision settings entry point" bug: "390500290" } + +flag { + name: "supervision_manager_apis" + is_exported: true + namespace: "supervision" + description: "Flag that enables system APIs in Supervision Manager" + bug: "382034839" +} diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index fcdb02ab5da2..ba1473cf5ed7 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -120,6 +120,16 @@ flag { } flag { + name: "correct_virtual_display_power_state" + namespace: "virtual_devices" + description: "Fix the virtual display power state" + bug: "371125136" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "vdm_settings" namespace: "virtual_devices" description: "Show virtual devices in Settings" diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 3391e79b2ae4..55d78f9b8c48 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -17,8 +17,8 @@ package android.content; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; -import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import static android.app.ondeviceintelligence.flags.Flags.FLAG_ENABLE_ON_DEVICE_INTELLIGENCE_MODULE; +import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; import static android.security.Flags.FLAG_SECURE_LOCKDOWN; import android.annotation.AttrRes; @@ -6858,6 +6858,8 @@ public abstract class Context { * @see android.app.supervision.SupervisionManager * @hide */ + @SystemApi + @FlaggedApi(android.app.supervision.flags.Flags.FLAG_SUPERVISION_MANAGER_APIS) public static final String SUPERVISION_SERVICE = "supervision"; /** diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index e6082d0df1f8..5c904c15e706 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -622,3 +622,10 @@ flag { description: "Add API to logout user" bug: "350045389" } + +flag { + name: "enable_moving_content_into_private_space" + namespace: "profile_experiences" + description: "Enable moving content into the Private Space" + bug: "360066001" +} diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index 075457885586..f538e9ffffdd 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -25,6 +25,7 @@ import android.content.res.loader.ResourcesProvider; import android.ravenwood.annotation.RavenwoodClassLoadHook; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.text.TextUtils; +import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -50,6 +51,7 @@ import java.util.Objects; @RavenwoodKeepWholeClass @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class ApkAssets { + private static final boolean DEBUG = false; /** * The apk assets contains framework resource values specified by the system. @@ -134,6 +136,17 @@ public final class ApkAssets { @Nullable private final AssetsProvider mAssets; + @NonNull + private String mName; + + private static final int UPTODATE_FALSE = 0; + private static final int UPTODATE_TRUE = 1; + private static final int UPTODATE_ALWAYS_TRUE = 2; + + // Start with the only value that may change later and would force a native call to + // double check it. + private int mPreviousUpToDateResult = UPTODATE_TRUE; + /** * Creates a new ApkAssets instance from the given path on disk. * @@ -304,7 +317,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, path); Objects.requireNonNull(path, "path"); mNativePtr = nativeLoad(format, path, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); @@ -313,7 +326,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, friendlyName); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets); @@ -323,7 +336,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, long offset, long length, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, friendlyName); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets); @@ -331,16 +344,17 @@ public final class ApkAssets { } private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) { - this(FORMAT_APK, flags, assets); + this(FORMAT_APK, flags, assets, "empty"); mNativePtr = nativeLoadEmpty(flags, assets); mStringBlock = null; } private ApkAssets(@FormatType int format, @PropertyFlags int flags, - @Nullable AssetsProvider assets) { + @Nullable AssetsProvider assets, @NonNull String name) { mFlags = flags; mAssets = assets; mIsOverlay = format == FORMAT_IDMAP; + if (DEBUG) mName = name; } @UnsupportedAppUsage @@ -421,13 +435,41 @@ public final class ApkAssets { } } + private static double intervalMs(long beginNs, long endNs) { + return (endNs - beginNs) / 1000000.0; + } + /** * Returns false if the underlying APK was changed since this ApkAssets was loaded. */ public boolean isUpToDate() { + // This function is performance-critical - it's called multiple times on every Resources + // object creation, and on few other cache accesses - so it's important to avoid the native + // call when we know for sure what it will return (which is the case for both ALWAYS_TRUE + // and FALSE). + if (mPreviousUpToDateResult != UPTODATE_TRUE) { + return mPreviousUpToDateResult == UPTODATE_ALWAYS_TRUE; + } + final long beforeTs, afterLockTs, afterNativeTs, afterUnlockTs; + if (DEBUG) beforeTs = System.nanoTime(); + final int res; synchronized (this) { - return nativeIsUpToDate(mNativePtr); + if (DEBUG) afterLockTs = System.nanoTime(); + res = nativeIsUpToDate(mNativePtr); + if (DEBUG) afterNativeTs = System.nanoTime(); + } + if (DEBUG) { + afterUnlockTs = System.nanoTime(); + if (afterUnlockTs - beforeTs >= 10L * 1000000) { + Log.d("ApkAssets", "isUpToDate(" + mName + ") took " + + intervalMs(beforeTs, afterUnlockTs) + + " ms: " + intervalMs(beforeTs, afterLockTs) + + " / " + intervalMs(afterLockTs, afterNativeTs) + + " / " + intervalMs(afterNativeTs, afterUnlockTs)); + } } + mPreviousUpToDateResult = res; + return res != UPTODATE_FALSE; } public boolean isSystem() { @@ -487,7 +529,7 @@ public final class ApkAssets { private static native @NonNull String nativeGetAssetPath(long ptr); private static native @NonNull String nativeGetDebugName(long ptr); private static native long nativeGetStringBlock(long ptr); - @CriticalNative private static native boolean nativeIsUpToDate(long ptr); + @CriticalNative private static native int nativeIsUpToDate(long ptr); private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException; private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr, String overlayableName) throws IOException; diff --git a/core/java/android/content/res/ResourceTimer.java b/core/java/android/content/res/ResourceTimer.java index d51f64ce8106..2d1bf4d9d296 100644 --- a/core/java/android/content/res/ResourceTimer.java +++ b/core/java/android/content/res/ResourceTimer.java @@ -17,13 +17,10 @@ package android.content.res; import android.annotation.NonNull; -import android.annotation.Nullable; - import android.app.AppProtoEnums; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; @@ -33,6 +30,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; +import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.Arrays; @@ -277,38 +275,40 @@ public final class ResourceTimer { * Update the metrics information and dump it. * @hide */ - public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) { - FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor()); - PrintWriter pw = new FastPrintWriter(fout); - synchronized (sLock) { - if (!sEnabled || (sConfig == null)) { + public static void dumpTimers(@NonNull FileDescriptor fd, String... args) { + try (PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd))) { + pw.println("\nDumping ResourceTimers"); + + final boolean enabled; + synchronized (sLock) { + enabled = sEnabled && sConfig != null; + } + if (!enabled) { pw.println(" Timers are not enabled in this process"); - pw.flush(); return; } - } - // Look for the --refresh switch. If the switch is present, then sTimers is updated. - // Otherwise, the current value of sTimers is displayed. - boolean refresh = Arrays.asList(args).contains("-refresh"); - - synchronized (sLock) { - update(refresh); - long runtime = sLastUpdated - sProcessStart; - pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); - for (int i = 0; i < sTimers.length; i++) { - Timer t = sTimers[i]; - if (t.count != 0) { - String name = sConfig.timers[i]; - pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " - + "largest=%s\n", - name, t.count, t.total / t.count, t.mintime, t.maxtime, - packedString(t.percentile), - packedString(t.largest)); + // Look for the --refresh switch. If the switch is present, then sTimers is updated. + // Otherwise, the current value of sTimers is displayed. + boolean refresh = Arrays.asList(args).contains("-refresh"); + + synchronized (sLock) { + update(refresh); + long runtime = sLastUpdated - sProcessStart; + pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); + for (int i = 0; i < sTimers.length; i++) { + Timer t = sTimers[i]; + if (t.count != 0) { + String name = sConfig.timers[i]; + pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " + + "largest=%s\n", + name, t.count, t.total / t.count, t.mintime, t.maxtime, + packedString(t.percentile), + packedString(t.largest)); + } } } } - pw.flush(); } // Enable (or disabled) the runtime timers. Note that timers are disabled by default. This diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 2ead17588be4..36fa05905814 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -346,7 +346,7 @@ public final class XmlBlock implements AutoCloseable { if (ev == ERROR_BAD_DOCUMENT) { throw new XmlPullParserException("Corrupt XML binary file"); } - if (Flags.layoutReadwriteFlags() && ev == START_TAG) { + if (useLayoutReadwrite() && ev == START_TAG) { AconfigFlags flags = ParsingPackageUtils.getAconfigFlags(); if (flags.skipCurrentElement(/* pkg= */ null, this)) { int depth = 1; @@ -388,6 +388,18 @@ public final class XmlBlock implements AutoCloseable { } return ev; } + + // Until ravenwood supports AconfigFlags, we just don't do layoutReadwriteFlags(). + @android.ravenwood.annotation.RavenwoodReplace( + bug = 396458006, blockedBy = AconfigFlags.class) + private static boolean useLayoutReadwrite() { + return Flags.layoutReadwriteFlags(); + } + + private static boolean useLayoutReadwrite$ravenwood() { + return false; + } + public void require(int type, String namespace, String name) throws XmlPullParserException,IOException { if (type != getEventType() || (namespace != null && !namespace.equals( getNamespace () ) ) diff --git a/core/java/android/hardware/display/ColorDisplayManager.java b/core/java/android/hardware/display/ColorDisplayManager.java index 0d9db1fa3c91..7debab946bc0 100644 --- a/core/java/android/hardware/display/ColorDisplayManager.java +++ b/core/java/android/hardware/display/ColorDisplayManager.java @@ -17,7 +17,6 @@ package android.hardware.display; import android.Manifest; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -401,7 +400,6 @@ public final class ColorDisplayManager { * @hide */ @TestApi - @FlaggedApi(android.app.Flags.FLAG_MODES_API) @RequiresPermission(Manifest.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS) public boolean isSaturationActivated() { return mManager.isSaturationActivated(); diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index d8919160320a..7850e377ec4d 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -612,6 +612,7 @@ public final class DisplayManager { PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS, PRIVATE_EVENT_TYPE_HDR_SDR_RATIO_CHANGED, PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED, + PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface PrivateEventType {} @@ -677,7 +678,7 @@ public final class DisplayManager { * through the {@link DisplayListener#onDisplayChanged} callback method. New brightness * values can be retrieved via {@link android.view.Display#getBrightnessInfo()}. * - * @see #registerDisplayListener(DisplayListener, Handler, long) + * @see #registerDisplayListener(DisplayListener, Handler, long, long) * * @hide */ @@ -690,7 +691,7 @@ public final class DisplayManager { * * Requires that {@link Display#isHdrSdrRatioAvailable()} is true. * - * @see #registerDisplayListener(DisplayListener, Handler, long) + * @see #registerDisplayListener(DisplayListener, Handler, long, long) * * @hide */ @@ -699,11 +700,19 @@ public final class DisplayManager { /** * Event type to register for a display's connection changed. * - * @see #registerDisplayListener(DisplayListener, Handler, long) + * @see #registerDisplayListener(DisplayListener, Handler, long, long) * @hide */ public static final long PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED = 1L << 2; + /** + * Event type to register for a display's committed state changes. + * + * @see #registerDisplayListener(DisplayListener, Handler, long, long) + * @hide + */ + public static final long PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 3; + /** @hide */ public DisplayManager(Context context) { diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 339dbf2c2029..a7d610e54e2c 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -113,7 +113,8 @@ public final class DisplayManagerGlobal { EVENT_DISPLAY_CONNECTED, EVENT_DISPLAY_DISCONNECTED, EVENT_DISPLAY_REFRESH_RATE_CHANGED, - EVENT_DISPLAY_STATE_CHANGED + EVENT_DISPLAY_STATE_CHANGED, + EVENT_DISPLAY_COMMITTED_STATE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface DisplayEvent {} @@ -128,6 +129,8 @@ public final class DisplayManagerGlobal { public static final int EVENT_DISPLAY_DISCONNECTED = 7; public static final int EVENT_DISPLAY_REFRESH_RATE_CHANGED = 8; public static final int EVENT_DISPLAY_STATE_CHANGED = 9; + public static final int EVENT_DISPLAY_COMMITTED_STATE_CHANGED = 10; + @LongDef(prefix = {"INTERNAL_EVENT_FLAG_"}, flag = true, value = { INTERNAL_EVENT_FLAG_DISPLAY_ADDED, @@ -139,6 +142,7 @@ public final class DisplayManagerGlobal { INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, INTERNAL_EVENT_FLAG_DISPLAY_STATE, INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED, + INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface InternalEventFlag {} @@ -152,6 +156,8 @@ public final class DisplayManagerGlobal { public static final long INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE = 1L << 6; public static final long INTERNAL_EVENT_FLAG_DISPLAY_STATE = 1L << 7; public static final long INTERNAL_EVENT_FLAG_TOPOLOGY_UPDATED = 1L << 8; + public static final long INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED = 1L << 9; + @UnsupportedAppUsage private static DisplayManagerGlobal sInstance; @@ -1550,6 +1556,12 @@ public final class DisplayManagerGlobal { mListener.onDisplayChanged(displayId); } break; + case EVENT_DISPLAY_COMMITTED_STATE_CHANGED: + if ((mInternalEventFlagsMask + & INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0) { + mListener.onDisplayChanged(displayId); + } + break; } if (DEBUG) { Trace.endSection(); @@ -1710,6 +1722,8 @@ public final class DisplayManagerGlobal { return "EVENT_DISPLAY_REFRESH_RATE_CHANGED"; case EVENT_DISPLAY_STATE_CHANGED: return "EVENT_DISPLAY_STATE_CHANGED"; + case EVENT_DISPLAY_COMMITTED_STATE_CHANGED: + return "EVENT_DISPLAY_COMMITTED_STATE_CHANGED"; } return "UNKNOWN"; } @@ -1756,6 +1770,13 @@ public final class DisplayManagerGlobal { & DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED) != 0) { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED; } + + if (Flags.committedStateSeparateEvent()) { + if ((privateEventFlags + & DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED) != 0) { + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED; + } + } return baseEventMask; } diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java index 48c5887d80d0..586830c8d189 100644 --- a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java +++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java @@ -224,6 +224,10 @@ public class FingerprintSensorConfigurations implements Parcelable { } catch (RemoteException e) { Log.d(TAG, "Unable to get sensor properties!"); } + + if (props == null) { + props = new SensorProps[]{}; + } return props; } } diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index 3d4b8854b01f..7c82abe083c2 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -386,7 +386,7 @@ public class InputSettings { */ public static boolean isTouchpadAccelerationEnabled(@NonNull Context context) { if (!isPointerAccelerationFeatureFlagEnabled()) { - return false; + return true; } return Settings.System.getIntForUser(context.getContentResolver(), @@ -833,7 +833,7 @@ public class InputSettings { */ public static boolean isMousePointerAccelerationEnabled(@NonNull Context context) { if (!isPointerAccelerationFeatureFlagEnabled()) { - return false; + return true; } return Settings.System.getIntForUser(context.getContentResolver(), diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 08365c935626..33bf4a29ecc6 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2776,7 +2776,7 @@ public class UserManager { } /** - * Returns whether logging out is currently allowed for the context user. + * Returns whether logging out is currently allowed for the specified user. * * <p>Logging out is not allowed in the following cases: * <ol> @@ -2794,11 +2794,10 @@ public class UserManager { * {@link #LOGOUTABILITY_STATUS_CANNOT_SWITCH}. * @hide */ - @UserHandleAware @RequiresPermission(Manifest.permission.MANAGE_USERS) - public @UserLogoutability int getUserLogoutability() { + public @UserLogoutability int getUserLogoutability(@UserIdInt int userId) { try { - return mService.getUserLogoutability(mUserId); + return mService.getUserLogoutability(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/os/vibrator/VibratorFrequencyProfile.java b/core/java/android/os/vibrator/VibratorFrequencyProfile.java index 2b5f9bf2a22e..a8ed81846663 100644 --- a/core/java/android/os/vibrator/VibratorFrequencyProfile.java +++ b/core/java/android/os/vibrator/VibratorFrequencyProfile.java @@ -51,8 +51,7 @@ public final class VibratorFrequencyProfile { Preconditions.checkArgument(!frequencyProfile.isEmpty(), "Frequency profile must not be empty"); mFrequencyProfile = frequencyProfile; - mFrequenciesOutputAcceleration = generateFrequencyToAccelerationMap( - frequencyProfile.getFrequenciesHz(), frequencyProfile.getOutputAccelerationsGs()); + mFrequenciesOutputAcceleration = generateFrequencyToAccelerationMap(mFrequencyProfile); } /** @@ -133,18 +132,21 @@ public final class VibratorFrequencyProfile { } private static SparseArray<Float> generateFrequencyToAccelerationMap( - float[] frequencies, float[] accelerations) { - SparseArray<Float> sparseArray = new SparseArray<>(frequencies.length); - + VibratorInfo.FrequencyProfile frequencyProfile) { + float[] frequencies = frequencyProfile.getFrequenciesHz(); + SparseArray<Float> frequencyToAcceleration = new SparseArray<>(frequencies.length); + int lastFrequency = -1; for (int i = 0; i < frequencies.length; i++) { int frequency = (int) frequencies[i]; - float acceleration = accelerations[i]; - - sparseArray.put(frequency, - Math.min(acceleration, sparseArray.get(frequency, Float.MAX_VALUE))); + if (frequency == lastFrequency) { + continue; // Skip duplicate frequencies + } + float acceleration = frequencyProfile.getOutputAccelerationGs(frequency); + frequencyToAcceleration.put(frequency, acceleration); + lastFrequency = frequency; } - return sparseArray; + return frequencyToAcceleration; } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 670709846d4c..1a9b42e46a1c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2119,7 +2119,6 @@ public final class Settings { * <p> * Output: Nothing. */ - @FlaggedApi(android.app.Flags.FLAG_MODES_API) @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS"; @@ -2129,7 +2128,6 @@ public final class Settings { * <p> * This must be passed as an extra field to the {@link #ACTION_AUTOMATIC_ZEN_RULE_SETTINGS}. */ - @FlaggedApi(android.app.Flags.FLAG_MODES_API) public static final String EXTRA_AUTOMATIC_ZEN_RULE_ID = "android.provider.extra.AUTOMATIC_ZEN_RULE_ID"; diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java index 9e02ecd19aee..903f8170104e 100644 --- a/core/java/android/security/FileIntegrityManager.java +++ b/core/java/android/security/FileIntegrityManager.java @@ -65,13 +65,7 @@ public final class FileIntegrityManager { * other fs-verity APIs. */ public boolean isApkVeritySupported() { - try { - // Go through the service just to avoid exposing the vendor controlled system property - // to all apps. - return mService.isApkVeritySupported(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return VerityUtils.isFsVeritySupported(); } /** diff --git a/core/java/android/security/IFileIntegrityService.aidl b/core/java/android/security/IFileIntegrityService.aidl index c6def239d59a..5a1a6a0ea6d9 100644 --- a/core/java/android/security/IFileIntegrityService.aidl +++ b/core/java/android/security/IFileIntegrityService.aidl @@ -24,8 +24,6 @@ import android.os.IInstalld; * @hide */ interface IFileIntegrityService { - boolean isApkVeritySupported(); - IInstalld.IFsveritySetupAuthToken createAuthToken(in ParcelFileDescriptor authFd); @EnforcePermission("SETUP_FSVERITY") diff --git a/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java index 76ee4480c222..f5f334891d2d 100644 --- a/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java +++ b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java @@ -19,10 +19,10 @@ package android.security.intrusiondetection; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.SystemApi; import android.app.admin.ConnectEvent; import android.app.admin.DnsEvent; import android.app.admin.SecurityLog.SecurityEvent; -import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.security.Flags; @@ -223,13 +223,13 @@ public final class IntrusionDetectionEvent implements Parcelable { out.writeInt(mType); switch (mType) { case SECURITY_EVENT: - out.writeParcelable(mSecurityEvent, flags); + mSecurityEvent.writeToParcel(out, flags); break; case NETWORK_EVENT_DNS: - out.writeParcelable(mNetworkEventDns, flags); + mNetworkEventDns.writeToParcel(out, flags); break; case NETWORK_EVENT_CONNECT: - out.writeParcelable(mNetworkEventConnect, flags); + mNetworkEventConnect.writeToParcel(out, flags); break; default: throw new IllegalArgumentException("Invalid event type: " + mType); diff --git a/core/java/android/service/notification/Condition.java b/core/java/android/service/notification/Condition.java index 6e771f8f0ffe..c375cfb900ac 100644 --- a/core/java/android/service/notification/Condition.java +++ b/core/java/android/service/notification/Condition.java @@ -18,11 +18,9 @@ package android.service.notification; import static com.android.internal.util.Preconditions.checkArgument; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.Flags; import android.content.Context; import android.net.Uri; import android.os.Parcel; @@ -105,20 +103,15 @@ public final class Condition implements Parcelable { public @interface Source {} /** The state is changing due to an unknown reason. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int SOURCE_UNKNOWN = 0; /** The state is changing due to an explicit user action. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int SOURCE_USER_ACTION = 1; /** The state is changing due to an automatic schedule (alarm, set time, etc). */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int SOURCE_SCHEDULE = 2; /** The state is changing due to a change in context (such as detected driving or sleeping). */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int SOURCE_CONTEXT = 3; /** The source of, or reason for, the state change represented by this Condition. **/ - @FlaggedApi(Flags.FLAG_MODES_API) public final @Source int source; // default = SOURCE_UNKNOWN /** @@ -145,7 +138,6 @@ public final class Condition implements Parcelable { * @param state whether the mode should be activated or deactivated * @param source the source of, or reason for, the state change represented by this Condition */ - @FlaggedApi(Flags.FLAG_MODES_API) public Condition(@Nullable Uri id, @Nullable String summary, @State int state, @Source int source) { this(id, summary, "", "", -1, state, source, FLAG_RELEVANT_ALWAYS); @@ -168,7 +160,6 @@ public final class Condition implements Parcelable { * @param source the source of, or reason for, the state change represented by this Condition * @param flags flags on this condition */ - @FlaggedApi(Flags.FLAG_MODES_API) public Condition(@Nullable Uri id, @Nullable String summary, @Nullable String line1, @Nullable String line2, int icon, @State int state, @Source int source, int flags) { @@ -195,15 +186,13 @@ public final class Condition implements Parcelable { source.readString(), source.readInt(), source.readInt(), - Flags.modesApi() ? source.readInt() : SOURCE_UNKNOWN, + source.readInt(), source.readInt()); } /** @hide */ public void validate() { - if (Flags.modesApi()) { - checkValidSource(source); - } + checkValidSource(source); } private static boolean isValidState(int state) { @@ -211,11 +200,9 @@ public final class Condition implements Parcelable { } private static int checkValidSource(@Source int source) { - if (Flags.modesApi()) { - checkArgument(source >= SOURCE_UNKNOWN && source <= SOURCE_CONTEXT, - "Condition source must be one of SOURCE_UNKNOWN, SOURCE_USER_ACTION, " - + "SOURCE_SCHEDULE, or SOURCE_CONTEXT"); - } + checkArgument(source >= SOURCE_UNKNOWN && source <= SOURCE_CONTEXT, + "Condition source must be one of SOURCE_UNKNOWN, SOURCE_USER_ACTION, " + + "SOURCE_SCHEDULE, or SOURCE_CONTEXT"); return source; } @@ -227,25 +214,21 @@ public final class Condition implements Parcelable { dest.writeString(line2); dest.writeInt(icon); dest.writeInt(state); - if (Flags.modesApi()) { - dest.writeInt(this.source); - } + dest.writeInt(this.source); dest.writeInt(this.flags); } @Override public String toString() { - StringBuilder sb = new StringBuilder(Condition.class.getSimpleName()).append('[') + return new StringBuilder(Condition.class.getSimpleName()).append('[') .append("state=").append(stateToString(state)) .append(",id=").append(id) .append(",summary=").append(summary) .append(",line1=").append(line1) .append(",line2=").append(line2) - .append(",icon=").append(icon); - if (Flags.modesApi()) { - sb.append(",source=").append(sourceToString(source)); - } - return sb.append(",flags=").append(flags) + .append(",icon=").append(icon) + .append(",source=").append(sourceToString(source)) + .append(",flags=").append(flags) .append(']').toString(); } @@ -279,7 +262,6 @@ public final class Condition implements Parcelable { * Provides a human-readable string version of the Source enum. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static @NonNull String sourceToString(@Source int source) { if (source == SOURCE_UNKNOWN) return "SOURCE_UNKNOWN"; if (source == SOURCE_USER_ACTION) return "SOURCE_USER_ACTION"; @@ -301,25 +283,19 @@ public final class Condition implements Parcelable { if (!(o instanceof Condition)) return false; if (o == this) return true; final Condition other = (Condition) o; - boolean finalEquals = Objects.equals(other.id, id) + return Objects.equals(other.id, id) && Objects.equals(other.summary, summary) && Objects.equals(other.line1, line1) && Objects.equals(other.line2, line2) && other.icon == icon && other.state == state - && other.flags == flags; - if (Flags.modesApi()) { - return finalEquals && other.source == source; - } - return finalEquals; + && other.flags == flags + && other.source == source; } @Override public int hashCode() { - if (Flags.modesApi()) { - return Objects.hash(id, summary, line1, line2, icon, state, source, flags); - } - return Objects.hash(id, summary, line1, line2, icon, state, flags); + return Objects.hash(id, summary, line1, line2, icon, state, source, flags); } @Override diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java index f11ce1621f93..fbee06e113fc 100644 --- a/core/java/android/service/notification/SystemZenRules.java +++ b/core/java/android/service/notification/SystemZenRules.java @@ -16,7 +16,6 @@ package android.service.notification; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; @@ -47,7 +46,6 @@ public final class SystemZenRules { public static final String PACKAGE_ANDROID = "android"; /** Updates existing system-owned rules to use the new Modes fields (type, etc). */ - @FlaggedApi(Flags.FLAG_MODES_API) public static void maybeUpgradeRules(Context context, ZenModeConfig config) { for (ZenRule rule : config.automaticRules.values()) { if (isSystemOwnedRule(rule)) { @@ -69,7 +67,6 @@ public final class SystemZenRules { return PACKAGE_ANDROID.equals(rule.pkg); } - @FlaggedApi(Flags.FLAG_MODES_API) private static void upgradeSystemProviderRule(Context context, ZenRule rule) { ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId); if (scheduleInfo != null) { diff --git a/core/java/android/service/notification/ZenAdapters.java b/core/java/android/service/notification/ZenAdapters.java index a122b7155b18..4f53bfa841ef 100644 --- a/core/java/android/service/notification/ZenAdapters.java +++ b/core/java/android/service/notification/ZenAdapters.java @@ -17,7 +17,6 @@ package android.service.notification; import android.annotation.NonNull; -import android.app.Flags; import android.app.NotificationManager.Policy; /** @@ -50,7 +49,8 @@ public class ZenAdapters { : ZenPolicy.PEOPLE_TYPE_NONE) .allowReminders(policy.allowReminders()) .allowRepeatCallers(policy.allowRepeatCallers()) - .allowSystem(policy.allowSystem()); + .allowSystem(policy.allowSystem()) + .allowPriorityChannels(policy.allowPriorityChannels()); if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) { zenPolicyBuilder.showBadges(policy.showBadges()) @@ -62,10 +62,6 @@ public class ZenAdapters { .showStatusBarIcons(policy.showStatusBarIcons()); } - if (Flags.modesApi()) { - zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels()); - } - return zenPolicyBuilder.build(); } diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java index 06bd2555c2f8..d88fb3e35b1e 100644 --- a/core/java/android/service/notification/ZenDeviceEffects.java +++ b/core/java/android/service/notification/ZenDeviceEffects.java @@ -16,12 +16,10 @@ package android.service.notification; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; -import android.app.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -37,7 +35,6 @@ import java.util.Set; * Represents the set of device effects (affecting display and device behavior in general) that * are applied whenever an {@link android.app.AutomaticZenRule} is active. */ -@FlaggedApi(Flags.FLAG_MODES_API) public final class ZenDeviceEffects implements Parcelable { /** @@ -157,7 +154,6 @@ public final class ZenDeviceEffects implements Parcelable { } /** @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public void validate() { int extraEffectsLength = 0; for (String extraEffect : mExtraEffects) { @@ -435,7 +431,6 @@ public final class ZenDeviceEffects implements Parcelable { } /** Builder class for {@link ZenDeviceEffects} objects. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final class Builder { private boolean mGrayscale; diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 4f459aa9131a..6f94c1b2d274 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -228,7 +228,7 @@ public class ZenModeConfig implements Parcelable { private static final boolean DEFAULT_ALLOW_CONV = true; private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; private static final boolean DEFAULT_ALLOW_PRIORITY_CHANNELS = true; - private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false; + private static final boolean DEFAULT_HAS_PRIORITY_CHANNELS = false; // Default setting here is 010011101 = 157 private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS = SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT @@ -242,9 +242,6 @@ public class ZenModeConfig implements Parcelable { public static final int XML_VERSION_MODES_API = 11; public static final int XML_VERSION_MODES_UI = 12; - // TODO: b/310620812, b/344831624 - Update XML_VERSION and update default_zen_config.xml - // accordingly when modes_api / modes_ui are inlined. - private static final int XML_VERSION_PRE_MODES = 10; public static final String ZEN_TAG = "zen"; private static final String ZEN_ATT_VERSION = "version"; private static final String ZEN_ATT_USER = "user"; @@ -269,7 +266,7 @@ public class ZenModeConfig implements Parcelable { private static final String DISALLOW_TAG = "disallow"; private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects"; private static final String STATE_TAG = "state"; - private static final String STATE_ATT_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd"; + private static final String STATE_HAS_PRIORITY_CHANNELS = "areChannelsBypassingDnd"; // zen policy visual effects attributes private static final String SHOW_ATT_FULL_SCREEN_INTENT = "showFullScreenIntent"; @@ -303,7 +300,6 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_CONDITION_ID = "conditionId"; private static final String RULE_ATT_CREATION_TIME = "creationTime"; private static final String RULE_ATT_ENABLER = "enabler"; - private static final String RULE_ATT_MODIFIED = "modified"; private static final String RULE_ATT_ALLOW_MANUAL = "userInvokable"; private static final String RULE_ATT_TYPE = "type"; private static final String RULE_ATT_USER_MODIFIED_FIELDS = "userModifiedFields"; @@ -313,6 +309,7 @@ public class ZenModeConfig implements Parcelable { private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin"; private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects"; private static final String RULE_ATT_CONDITION_OVERRIDE = "conditionOverride"; + private static final String RULE_ATT_LAST_ACTIVATION = "lastActivation"; private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale"; private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY = @@ -348,11 +345,11 @@ public class ZenModeConfig implements Parcelable { public int allowConversationsFrom = DEFAULT_ALLOW_CONV_FROM; public int user = UserHandle.USER_SYSTEM; public int suppressedVisualEffects = DEFAULT_SUPPRESSED_VISUAL_EFFECTS; - // Note that when the modes_api flag is true, the areChannelsBypassingDnd boolean only tracks - // whether the current user has any priority channels. These channels may bypass DND when - // allowPriorityChannels is true. - // TODO: b/310620812 - Rename to be more accurate when modes_api flag is inlined. - public boolean areChannelsBypassingDnd = DEFAULT_CHANNELS_BYPASSING_DND; + /** + * Whether the current user has any priority channels. These channels may bypass DND when + * {@link #allowPriorityChannels} is true. + */ + public boolean hasPriorityChannels = DEFAULT_HAS_PRIORITY_CHANNELS; public boolean allowPriorityChannels = DEFAULT_ALLOW_PRIORITY_CHANNELS; public int version; @@ -384,22 +381,18 @@ public class ZenModeConfig implements Parcelable { user = source.readInt(); manualRule = source.readParcelable(null, ZenRule.class); readRulesFromParcel(automaticRules, source); - if (Flags.modesApi()) { - readRulesFromParcel(deletedRules, source); - } + readRulesFromParcel(deletedRules, source); if (!Flags.modesUi()) { allowAlarms = source.readInt() == 1; allowMedia = source.readInt() == 1; allowSystem = source.readInt() == 1; suppressedVisualEffects = source.readInt(); } - areChannelsBypassingDnd = source.readInt() == 1; + hasPriorityChannels = source.readInt() == 1; if (!Flags.modesUi()) { allowConversations = source.readBoolean(); allowConversationsFrom = source.readInt(); - if (Flags.modesApi()) { - allowPriorityChannels = source.readBoolean(); - } + allowPriorityChannels = source.readBoolean(); } } @@ -493,22 +486,18 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(user); dest.writeParcelable(manualRule, 0); writeRulesToParcel(automaticRules, dest); - if (Flags.modesApi()) { - writeRulesToParcel(deletedRules, dest); - } + writeRulesToParcel(deletedRules, dest); if (!Flags.modesUi()) { dest.writeInt(allowAlarms ? 1 : 0); dest.writeInt(allowMedia ? 1 : 0); dest.writeInt(allowSystem ? 1 : 0); dest.writeInt(suppressedVisualEffects); } - dest.writeInt(areChannelsBypassingDnd ? 1 : 0); + dest.writeInt(hasPriorityChannels ? 1 : 0); if (!Flags.modesUi()) { dest.writeBoolean(allowConversations); dest.writeInt(allowConversationsFrom); - if (Flags.modesApi()) { - dest.writeBoolean(allowPriorityChannels); - } + dest.writeBoolean(allowPriorityChannels); } } @@ -549,17 +538,13 @@ public class ZenModeConfig implements Parcelable { (allowConversationsFrom)) .append("\nsuppressedVisualEffects=").append(suppressedVisualEffects); } - if (Flags.modesApi()) { - sb.append("\nhasPriorityChannels=").append(areChannelsBypassingDnd); - sb.append(",allowPriorityChannels=").append(allowPriorityChannels); - } else { - sb.append("\nareChannelsBypassingDnd=").append(areChannelsBypassingDnd); - } + + sb.append("\nhasPriorityChannels=").append(hasPriorityChannels); + sb.append(",allowPriorityChannels=").append(allowPriorityChannels); sb.append(",\nautomaticRules=").append(rulesToString(automaticRules)); sb.append(",\nmanualRule=").append(manualRule); - if (Flags.modesApi()) { - sb.append(",\ndeletedRules=").append(rulesToString(deletedRules)); - } + sb.append(",\ndeletedRules=").append(rulesToString(deletedRules)); + return sb.append(']').toString(); } @@ -854,7 +839,7 @@ public class ZenModeConfig implements Parcelable { final ZenModeConfig other = (ZenModeConfig) o; // The policy fields that live on config are compared directly because the fields will // contain data until MODES_UI is rolled out/cleaned up. - boolean eq = other.allowAlarms == allowAlarms + return other.allowAlarms == allowAlarms && other.allowMedia == allowMedia && other.allowSystem == allowSystem && other.allowCalls == allowCalls @@ -868,35 +853,23 @@ public class ZenModeConfig implements Parcelable { && Objects.equals(other.automaticRules, automaticRules) && Objects.equals(other.manualRule, manualRule) && other.suppressedVisualEffects == suppressedVisualEffects - && other.areChannelsBypassingDnd == areChannelsBypassingDnd + && other.hasPriorityChannels == hasPriorityChannels && other.allowConversations == allowConversations - && other.allowConversationsFrom == allowConversationsFrom; - if (Flags.modesApi()) { - return eq - && Objects.equals(other.deletedRules, deletedRules) - && other.allowPriorityChannels == allowPriorityChannels; - } - return eq; + && other.allowConversationsFrom == allowConversationsFrom + && Objects.equals(other.deletedRules, deletedRules) + && other.allowPriorityChannels == allowPriorityChannels; } @Override public int hashCode() { // The policy fields that live on config are compared directly because the fields will // contain data until MODES_UI is rolled out/cleaned up. - if (Flags.modesApi()) { - return Objects.hash(allowAlarms, allowMedia, allowSystem, allowCalls, - allowRepeatCallers, allowMessages, - allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents, - user, automaticRules, manualRule, - suppressedVisualEffects, areChannelsBypassingDnd, allowConversations, - allowConversationsFrom, allowPriorityChannels); - } return Objects.hash(allowAlarms, allowMedia, allowSystem, allowCalls, allowRepeatCallers, allowMessages, allowCallsFrom, allowMessagesFrom, allowReminders, allowEvents, user, automaticRules, manualRule, - suppressedVisualEffects, areChannelsBypassingDnd, allowConversations, - allowConversationsFrom); + suppressedVisualEffects, hasPriorityChannels, allowConversations, + allowConversationsFrom, allowPriorityChannels); } private static String toDayList(int[] days) { @@ -952,10 +925,8 @@ public class ZenModeConfig implements Parcelable { public static int getCurrentXmlVersion() { if (Flags.modesUi()) { return XML_VERSION_MODES_UI; - } else if (Flags.modesApi()) { - return XML_VERSION_MODES_API; } else { - return XML_VERSION_PRE_MODES; + return XML_VERSION_MODES_API; } } @@ -1006,10 +977,8 @@ public class ZenModeConfig implements Parcelable { rt.allowMedia = safeBoolean(parser, ALLOW_ATT_MEDIA, DEFAULT_ALLOW_MEDIA); rt.allowSystem = safeBoolean(parser, ALLOW_ATT_SYSTEM, DEFAULT_ALLOW_SYSTEM); - if (Flags.modesApi()) { - rt.allowPriorityChannels = safeBoolean(parser, ALLOW_ATT_CHANNELS, - DEFAULT_ALLOW_PRIORITY_CHANNELS); - } + rt.allowPriorityChannels = safeBoolean(parser, ALLOW_ATT_CHANNELS, + DEFAULT_ALLOW_PRIORITY_CHANNELS); // migrate old suppressed visual effects fields, if they still exist in the xml Boolean allowWhenScreenOff = unsafeBoolean(parser, ALLOW_ATT_SCREEN_OFF); @@ -1054,13 +1023,12 @@ public class ZenModeConfig implements Parcelable { } else { readRuleCount++; } - } else if (AUTOMATIC_TAG.equals(tag) - || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) { + } else if (AUTOMATIC_TAG.equals(tag) || AUTOMATIC_DELETED_TAG.equals(tag)) { final String id = parser.getAttributeValue(null, RULE_ATT_ID); if (id != null) { final ZenRule automaticRule = readRuleXml(parser); automaticRule.id = id; - if (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag)) { + if (AUTOMATIC_DELETED_TAG.equals(tag)) { String deletedRuleKey = deletedRuleKey(automaticRule); if (deletedRuleKey != null) { rt.deletedRules.put(deletedRuleKey, automaticRule); @@ -1071,8 +1039,8 @@ public class ZenModeConfig implements Parcelable { } } } else if (STATE_TAG.equals(tag)) { - rt.areChannelsBypassingDnd = safeBoolean(parser, - STATE_ATT_CHANNELS_BYPASSING_DND, DEFAULT_CHANNELS_BYPASSING_DND); + rt.hasPriorityChannels = safeBoolean(parser, + STATE_HAS_PRIORITY_CHANNELS, DEFAULT_HAS_PRIORITY_CHANNELS); } } if (type == XmlPullParser.END_TAG && ZEN_TAG.equals(tag)) { @@ -1149,9 +1117,7 @@ public class ZenModeConfig implements Parcelable { out.attributeBoolean(null, ALLOW_ATT_SYSTEM, allowSystem); out.attributeBoolean(null, ALLOW_ATT_CONV, allowConversations); out.attributeInt(null, ALLOW_ATT_CONV_FROM, allowConversationsFrom); - if (Flags.modesApi()) { - out.attributeBoolean(null, ALLOW_ATT_CHANNELS, allowPriorityChannels); - } + out.attributeBoolean(null, ALLOW_ATT_CHANNELS, allowPriorityChannels); out.endTag(null, ALLOW_TAG); out.startTag(null, DISALLOW_TAG); @@ -1174,7 +1140,7 @@ public class ZenModeConfig implements Parcelable { out.endTag(null, AUTOMATIC_TAG); writtenRuleCount++; } - if (Flags.modesApi() && !forBackup) { + if (!forBackup) { for (int i = 0; i < deletedRules.size(); i++) { final ZenRule deletedRule = deletedRules.valueAt(i); out.startTag(null, AUTOMATIC_DELETED_TAG); @@ -1185,7 +1151,7 @@ public class ZenModeConfig implements Parcelable { } out.startTag(null, STATE_TAG); - out.attributeBoolean(null, STATE_ATT_CHANNELS_BYPASSING_DND, areChannelsBypassingDnd); + out.attributeBoolean(null, STATE_HAS_PRIORITY_CHANNELS, hasPriorityChannels); out.endTag(null, STATE_TAG); out.endTag(null, ZEN_TAG); @@ -1212,39 +1178,29 @@ public class ZenModeConfig implements Parcelable { rt.creationTime = safeLong(parser, RULE_ATT_CREATION_TIME, 0); rt.enabler = parser.getAttributeValue(null, RULE_ATT_ENABLER); rt.condition = readConditionXml(parser); - - if (!Flags.modesApi() && rt.zenMode != ZEN_MODE_IMPORTANT_INTERRUPTIONS - && Condition.isValidId(rt.conditionId, SYSTEM_AUTHORITY)) { - // all default rules and user created rules updated to zenMode important interruptions - Slog.i(TAG, "Updating zenMode of automatic rule " + rt.name); - rt.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - } - rt.modified = safeBoolean(parser, RULE_ATT_MODIFIED, false); rt.zenPolicy = readZenPolicyXml(parser); - if (Flags.modesApi()) { - rt.zenDeviceEffects = readZenDeviceEffectsXml(parser); - rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false); - rt.iconResName = parser.getAttributeValue(null, RULE_ATT_ICON); - rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC); - rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN); - rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0); - rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0); - rt.zenDeviceEffectsUserModifiedFields = safeInt(parser, - DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0); - Long deletionInstant = tryParseLong( - parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null); - if (deletionInstant != null) { - rt.deletionInstant = Instant.ofEpochMilli(deletionInstant); - } - if (Flags.modesUi()) { - rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN, - ORIGIN_UNKNOWN); - rt.legacySuppressedEffects = safeInt(parser, - RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0); - rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE, - ZenRule.OVERRIDE_NONE); + rt.zenDeviceEffects = readZenDeviceEffectsXml(parser); + rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false); + rt.iconResName = parser.getAttributeValue(null, RULE_ATT_ICON); + rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC); + rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN); + rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0); + rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0); + rt.zenDeviceEffectsUserModifiedFields = safeInt(parser, + DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0); + rt.deletionInstant = safeInstant(parser, RULE_ATT_DELETION_INSTANT, null); + if (Flags.modesUi()) { + rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN, + ORIGIN_UNKNOWN); + rt.legacySuppressedEffects = safeInt(parser, + RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0); + rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE, + ZenRule.OVERRIDE_NONE); + if (Flags.modesCleanupImplicit()) { + rt.lastActivation = safeInstant(parser, RULE_ATT_LAST_ACTIVATION, null); } } + return rt; } @@ -1278,38 +1234,42 @@ public class ZenModeConfig implements Parcelable { if (rule.zenPolicy != null) { writeZenPolicyXml(rule.zenPolicy, out); } - if (Flags.modesApi() && rule.zenDeviceEffects != null) { + if (rule.zenDeviceEffects != null) { writeZenDeviceEffectsXml(rule.zenDeviceEffects, out); } - out.attributeBoolean(null, RULE_ATT_MODIFIED, rule.modified); - if (Flags.modesApi()) { - out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation); - if (rule.iconResName != null) { - out.attribute(null, RULE_ATT_ICON, rule.iconResName); - } - if (rule.triggerDescription != null) { - out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription); - } - out.attributeInt(null, RULE_ATT_TYPE, rule.type); - out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields); - out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields); - out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, - rule.zenDeviceEffectsUserModifiedFields); - if (rule.deletionInstant != null) { - out.attributeLong(null, RULE_ATT_DELETION_INSTANT, - rule.deletionInstant.toEpochMilli()); + out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation); + if (rule.iconResName != null) { + out.attribute(null, RULE_ATT_ICON, rule.iconResName); + } + if (rule.triggerDescription != null) { + out.attribute(null, RULE_ATT_TRIGGER_DESC, rule.triggerDescription); + } + out.attributeInt(null, RULE_ATT_TYPE, rule.type); + out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields); + out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields); + out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, + rule.zenDeviceEffectsUserModifiedFields); + writeXmlAttributeInstant(out, RULE_ATT_DELETION_INSTANT, rule.deletionInstant); + if (Flags.modesUi()) { + out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin); + out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, + rule.legacySuppressedEffects); + if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) { + out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride); } - if (Flags.modesUi()) { - out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin); - out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, - rule.legacySuppressedEffects); - if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) { - out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride); - } + if (Flags.modesCleanupImplicit()) { + writeXmlAttributeInstant(out, RULE_ATT_LAST_ACTIVATION, rule.lastActivation); } } } + private static void writeXmlAttributeInstant(TypedXmlSerializer out, String att, + @Nullable Instant instant) throws IOException { + if (instant != null) { + out.attributeLong(null, att, instant.toEpochMilli()); + } + } + public static Condition readConditionXml(TypedXmlPullParser parser) { final Uri id = safeUri(parser, CONDITION_ATT_ID); if (id == null) return null; @@ -1320,12 +1280,8 @@ public class ZenModeConfig implements Parcelable { final int state = safeInt(parser, CONDITION_ATT_STATE, -1); final int flags = safeInt(parser, CONDITION_ATT_FLAGS, -1); try { - if (Flags.modesApi()) { - final int source = safeInt(parser, CONDITION_ATT_SOURCE, Condition.SOURCE_UNKNOWN); - return new Condition(id, summary, line1, line2, icon, state, source, flags); - } else { - return new Condition(id, summary, line1, line2, icon, state, flags); - } + final int source = safeInt(parser, CONDITION_ATT_SOURCE, Condition.SOURCE_UNKNOWN); + return new Condition(id, summary, line1, line2, icon, state, source, flags); } catch (IllegalArgumentException e) { Slog.w(TAG, "Unable to read condition xml", e); return null; @@ -1339,9 +1295,7 @@ public class ZenModeConfig implements Parcelable { out.attribute(null, CONDITION_ATT_LINE2, c.line2); out.attributeInt(null, CONDITION_ATT_ICON, c.icon); out.attributeInt(null, CONDITION_ATT_STATE, c.state); - if (Flags.modesApi()) { - out.attributeInt(null, CONDITION_ATT_SOURCE, c.source); - } + out.attributeInt(null, CONDITION_ATT_SOURCE, c.source); out.attributeInt(null, CONDITION_ATT_FLAGS, c.flags); } @@ -1363,12 +1317,11 @@ public class ZenModeConfig implements Parcelable { final int system = safeInt(parser, ALLOW_ATT_SYSTEM, ZenPolicy.STATE_UNSET); final int events = safeInt(parser, ALLOW_ATT_EVENTS, ZenPolicy.STATE_UNSET); final int reminders = safeInt(parser, ALLOW_ATT_REMINDERS, ZenPolicy.STATE_UNSET); - if (Flags.modesApi()) { - final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET); - if (channels != ZenPolicy.STATE_UNSET) { - builder.allowPriorityChannels(channels == STATE_ALLOW); - policySet = true; - } + final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET); + + if (channels != ZenPolicy.STATE_UNSET) { + builder.allowPriorityChannels(channels == STATE_ALLOW); + policySet = true; } if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) { @@ -1478,10 +1431,7 @@ public class ZenModeConfig implements Parcelable { writeZenPolicyState(SHOW_ATT_AMBIENT, policy.getVisualEffectAmbient(), out); writeZenPolicyState(SHOW_ATT_NOTIFICATION_LIST, policy.getVisualEffectNotificationList(), out); - - if (Flags.modesApi()) { - writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out); - } + writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out); } private static void writeZenPolicyState(String attr, int val, TypedXmlSerializer out) @@ -1495,7 +1445,7 @@ public class ZenModeConfig implements Parcelable { if (val != ZenPolicy.CONVERSATION_SENDERS_UNSET) { out.attributeInt(null, attr, val); } - } else if (Flags.modesApi() && Objects.equals(attr, ALLOW_ATT_CHANNELS)) { + } else if (Objects.equals(attr, ALLOW_ATT_CHANNELS)) { if (val != ZenPolicy.STATE_UNSET) { out.attributeInt(null, attr, val); } @@ -1506,7 +1456,6 @@ public class ZenModeConfig implements Parcelable { } } - @FlaggedApi(Flags.FLAG_MODES_API) @Nullable private static ZenDeviceEffects readZenDeviceEffectsXml(TypedXmlPullParser parser) { ZenDeviceEffects deviceEffects = @@ -1539,7 +1488,6 @@ public class ZenModeConfig implements Parcelable { return deviceEffects.hasEffects() ? deviceEffects : null; } - @FlaggedApi(Flags.FLAG_MODES_API) private static void writeZenDeviceEffectsXml(ZenDeviceEffects deviceEffects, TypedXmlSerializer out) throws IOException { writeBooleanIfTrue(out, DEVICE_EFFECT_DISPLAY_GRAYSCALE, @@ -1659,6 +1607,19 @@ public class ZenModeConfig implements Parcelable { return values; } + @Nullable + private static Instant safeInstant(TypedXmlPullParser parser, String att, + @Nullable Instant defValue) { + final String strValue = parser.getAttributeValue(null, att); + if (!TextUtils.isEmpty(strValue)) { + Long longValue = tryParseLong(strValue, null); + if (longValue != null) { + return Instant.ofEpochMilli(longValue); + } + } + return defValue; + } + @Override public int describeContents() { return 0; @@ -1732,9 +1693,7 @@ public class ZenModeConfig implements Parcelable { (suppressedVisualEffects & Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) == 0); } - if (Flags.modesApi()) { - builder.allowPriorityChannels(allowPriorityChannels); - } + builder.allowPriorityChannels(allowPriorityChannels); return builder.build(); } @@ -1860,12 +1819,9 @@ public class ZenModeConfig implements Parcelable { suppressedVisualEffects |= Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; } - int state = defaultPolicy.state; - if (Flags.modesApi()) { - state = Policy.policyState(defaultPolicy.hasPriorityChannels(), - ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(), - DEFAULT_ALLOW_PRIORITY_CHANNELS)); - } + int state = Policy.policyState(defaultPolicy.hasPriorityChannels(), + ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(), + DEFAULT_ALLOW_PRIORITY_CHANNELS)); return new NotificationManager.Policy(priorityCategories, callSenders, messageSenders, suppressedVisualEffects, state, conversationSenders); @@ -1930,7 +1886,7 @@ public class ZenModeConfig implements Parcelable { priorityMessageSenders = peopleTypeToPrioritySenders( manualRule.zenPolicy.getPriorityMessageSenders(), DEFAULT_SOURCE); - state = Policy.policyState(areChannelsBypassingDnd, + state = Policy.policyState(hasPriorityChannels, manualRule.zenPolicy.getPriorityChannelsAllowed() != STATE_DISALLOW); boolean suppressFullScreenIntent = !manualRule.zenPolicy.isVisualEffectAllowed( @@ -2030,10 +1986,7 @@ public class ZenModeConfig implements Parcelable { priorityConversationSenders = zenPolicyConversationSendersToNotificationPolicy( getAllowConversationsFrom(), priorityConversationSenders); - state = areChannelsBypassingDnd ? Policy.STATE_CHANNELS_BYPASSING_DND : 0; - if (Flags.modesApi()) { - state = Policy.policyState(areChannelsBypassingDnd, allowPriorityChannels); - } + state = Policy.policyState(hasPriorityChannels, allowPriorityChannels); suppressedVisualEffects = getSuppressedVisualEffects(); } @@ -2114,13 +2067,11 @@ public class ZenModeConfig implements Parcelable { policy.priorityConversationSenders, allowConversationsFrom); if (policy.state != Policy.STATE_UNSET) { - if (Flags.modesApi()) { - setAllowPriorityChannels(policy.allowPriorityChannels()); - } + setAllowPriorityChannels(policy.allowPriorityChannels()); } } if (policy.state != Policy.STATE_UNSET) { - areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0; + hasPriorityChannels = (policy.state & Policy.STATE_HAS_PRIORITY_CHANNELS) != 0; } } @@ -2618,8 +2569,9 @@ public class ZenModeConfig implements Parcelable { @UnsupportedAppUsage public boolean enabled; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + // TODO: b/368247671 - Obsolete with MODES_UI; delete when the flag is inlined @Deprecated - public boolean snoozing; // user manually disabled this instance. Obsolete with MODES_UI + public boolean snoozing; // user manually disabled this instance. @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public String name; // required for automatic @UnsupportedAppUsage @@ -2635,9 +2587,7 @@ public class ZenModeConfig implements Parcelable { // package name, only used for manual rules when they have turned DND on. public String enabler; public ZenPolicy zenPolicy; - @FlaggedApi(Flags.FLAG_MODES_API) @Nullable public ZenDeviceEffects zenDeviceEffects; - public boolean modified; // rule has been modified from initial creation public String pkg; @AutomaticZenRule.Type public int type = AutomaticZenRule.TYPE_UNKNOWN; @@ -2668,6 +2618,18 @@ public class ZenModeConfig implements Parcelable { @ConditionOverride int conditionOverride = OVERRIDE_NONE; + /** + * Last time at which the rule was activated (for any reason, including overrides). + * If {@code null}, the rule has never been activated since its creation. + * + * <p>Note that this was previously untracked, so it will also be {@code null} for rules + * created before we started tracking and never activated since -- make sure to account for + * it, for example by falling back to {@link #creationTime} in logic involving this field. + */ + @Nullable + @FlaggedApi(Flags.FLAG_MODES_CLEANUP_IMPLICIT) + public Instant lastActivation; + public ZenRule() { } public ZenRule(Parcel source) { @@ -2689,46 +2651,45 @@ public class ZenModeConfig implements Parcelable { enabler = source.readString(); } zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class); - if (Flags.modesApi()) { - zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class); - } - modified = source.readInt() == 1; + zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class); pkg = source.readString(); - if (Flags.modesApi()) { - allowManualInvocation = source.readBoolean(); - iconResName = source.readString(); - triggerDescription = source.readString(); - type = source.readInt(); - userModifiedFields = source.readInt(); - zenPolicyUserModifiedFields = source.readInt(); - zenDeviceEffectsUserModifiedFields = source.readInt(); - if (source.readInt() == 1) { - deletionInstant = Instant.ofEpochMilli(source.readLong()); - } - if (Flags.modesUi()) { - disabledOrigin = source.readInt(); - legacySuppressedEffects = source.readInt(); - conditionOverride = source.readInt(); + allowManualInvocation = source.readBoolean(); + iconResName = source.readString(); + triggerDescription = source.readString(); + type = source.readInt(); + userModifiedFields = source.readInt(); + zenPolicyUserModifiedFields = source.readInt(); + zenDeviceEffectsUserModifiedFields = source.readInt(); + if (source.readInt() == 1) { + deletionInstant = Instant.ofEpochMilli(source.readLong()); + } + if (Flags.modesUi()) { + disabledOrigin = source.readInt(); + legacySuppressedEffects = source.readInt(); + conditionOverride = source.readInt(); + if (Flags.modesCleanupImplicit()) { + if (source.readInt() == 1) { + lastActivation = Instant.ofEpochMilli(source.readLong()); + } } } } /** - * Whether this ZenRule can be updated by an app. In general, rules that have been - * customized by the user cannot be further updated by an app, with some exceptions: + * Whether this ZenRule has been customized by the user in any way. + + * <p>In general, rules that have been customized by the user cannot be further updated by + * an app, with some exceptions: * <ul> * <li>Non user-configurable fields, like type, icon, configurationActivity, etc. * <li>Name, if the name was not specifically modified by the user (to support language * switches). * </ul> */ - @FlaggedApi(Flags.FLAG_MODES_API) - public boolean canBeUpdatedByApp() { - // The rule is considered updateable if its bitmask has no user modifications, and - // the bitmasks of the policy and device effects have no modification. - return userModifiedFields == 0 - && zenPolicyUserModifiedFields == 0 - && zenDeviceEffectsUserModifiedFields == 0; + public boolean isUserModified() { + return userModifiedFields != 0 + || zenPolicyUserModifiedFields != 0 + || zenDeviceEffectsUserModifiedFields != 0; } @Override @@ -2765,29 +2726,32 @@ public class ZenModeConfig implements Parcelable { dest.writeInt(0); } dest.writeParcelable(zenPolicy, 0); - if (Flags.modesApi()) { - dest.writeParcelable(zenDeviceEffects, 0); - } - dest.writeInt(modified ? 1 : 0); + dest.writeParcelable(zenDeviceEffects, 0); dest.writeString(pkg); - if (Flags.modesApi()) { - dest.writeBoolean(allowManualInvocation); - dest.writeString(iconResName); - dest.writeString(triggerDescription); - dest.writeInt(type); - dest.writeInt(userModifiedFields); - dest.writeInt(zenPolicyUserModifiedFields); - dest.writeInt(zenDeviceEffectsUserModifiedFields); - if (deletionInstant != null) { - dest.writeInt(1); - dest.writeLong(deletionInstant.toEpochMilli()); - } else { - dest.writeInt(0); - } - if (Flags.modesUi()) { - dest.writeInt(disabledOrigin); - dest.writeInt(legacySuppressedEffects); - dest.writeInt(conditionOverride); + dest.writeBoolean(allowManualInvocation); + dest.writeString(iconResName); + dest.writeString(triggerDescription); + dest.writeInt(type); + dest.writeInt(userModifiedFields); + dest.writeInt(zenPolicyUserModifiedFields); + dest.writeInt(zenDeviceEffectsUserModifiedFields); + if (deletionInstant != null) { + dest.writeInt(1); + dest.writeLong(deletionInstant.toEpochMilli()); + } else { + dest.writeInt(0); + } + if (Flags.modesUi()) { + dest.writeInt(disabledOrigin); + dest.writeInt(legacySuppressedEffects); + dest.writeInt(conditionOverride); + if (Flags.modesCleanupImplicit()) { + if (lastActivation != null) { + dest.writeInt(1); + dest.writeLong(lastActivation.toEpochMilli()); + } else { + dest.writeInt(0); + } } } } @@ -2816,34 +2780,33 @@ public class ZenModeConfig implements Parcelable { .append(",creationTime=").append(creationTime) .append(",enabler=").append(enabler) .append(",zenPolicy=").append(zenPolicy) - .append(",modified=").append(modified) - .append(",condition=").append(condition); - - if (Flags.modesApi()) { - sb.append(",deviceEffects=").append(zenDeviceEffects) - .append(",allowManualInvocation=").append(allowManualInvocation) - .append(",iconResName=").append(iconResName) - .append(",triggerDescription=").append(triggerDescription) - .append(",type=").append(type); - if (userModifiedFields != 0) { - sb.append(",userModifiedFields=") - .append(AutomaticZenRule.fieldsToString(userModifiedFields)); - } - if (zenPolicyUserModifiedFields != 0) { - sb.append(",zenPolicyUserModifiedFields=") - .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields)); - } - if (zenDeviceEffectsUserModifiedFields != 0) { - sb.append(",zenDeviceEffectsUserModifiedFields=") - .append(ZenDeviceEffects.fieldsToString( - zenDeviceEffectsUserModifiedFields)); - } - if (deletionInstant != null) { - sb.append(",deletionInstant=").append(deletionInstant); - } - if (Flags.modesUi()) { - sb.append(",disabledOrigin=").append(disabledOrigin); - sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects); + .append(",condition=").append(condition) + .append(",deviceEffects=").append(zenDeviceEffects) + .append(",allowManualInvocation=").append(allowManualInvocation) + .append(",iconResName=").append(iconResName) + .append(",triggerDescription=").append(triggerDescription) + .append(",type=").append(type); + if (userModifiedFields != 0) { + sb.append(",userModifiedFields=") + .append(AutomaticZenRule.fieldsToString(userModifiedFields)); + } + if (zenPolicyUserModifiedFields != 0) { + sb.append(",zenPolicyUserModifiedFields=") + .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields)); + } + if (zenDeviceEffectsUserModifiedFields != 0) { + sb.append(",zenDeviceEffectsUserModifiedFields=") + .append(ZenDeviceEffects.fieldsToString( + zenDeviceEffectsUserModifiedFields)); + } + if (deletionInstant != null) { + sb.append(",deletionInstant=").append(deletionInstant); + } + if (Flags.modesUi()) { + sb.append(",disabledOrigin=").append(disabledOrigin); + sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects); + if (Flags.modesCleanupImplicit()) { + sb.append(",lastActivation=").append(lastActivation); } } @@ -2869,7 +2832,7 @@ public class ZenModeConfig implements Parcelable { proto.write(ZenRuleProto.CREATION_TIME_MS, creationTime); proto.write(ZenRuleProto.ENABLED, enabled); proto.write(ZenRuleProto.ENABLER, enabler); - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { proto.write(ZenRuleProto.IS_SNOOZING, conditionOverride == OVERRIDE_DEACTIVATE); } else { proto.write(ZenRuleProto.IS_SNOOZING, snoozing); @@ -2887,7 +2850,6 @@ public class ZenModeConfig implements Parcelable { if (zenPolicy != null) { zenPolicy.dumpDebug(proto, ZenRuleProto.ZEN_POLICY); } - proto.write(ZenRuleProto.MODIFIED, modified); proto.end(token); } @@ -2908,26 +2870,25 @@ public class ZenModeConfig implements Parcelable { && Objects.equals(other.enabler, enabler) && Objects.equals(other.zenPolicy, zenPolicy) && Objects.equals(other.pkg, pkg) - && other.modified == modified; + && Objects.equals(other.zenDeviceEffects, zenDeviceEffects) + && other.allowManualInvocation == allowManualInvocation + && Objects.equals(other.iconResName, iconResName) + && Objects.equals(other.triggerDescription, triggerDescription) + && other.type == type + && other.userModifiedFields == userModifiedFields + && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields + && other.zenDeviceEffectsUserModifiedFields + == zenDeviceEffectsUserModifiedFields + && Objects.equals(other.deletionInstant, deletionInstant); - if (Flags.modesApi()) { + if (Flags.modesUi()) { finalEquals = finalEquals - && Objects.equals(other.zenDeviceEffects, zenDeviceEffects) - && other.allowManualInvocation == allowManualInvocation - && Objects.equals(other.iconResName, iconResName) - && Objects.equals(other.triggerDescription, triggerDescription) - && other.type == type - && other.userModifiedFields == userModifiedFields - && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields - && other.zenDeviceEffectsUserModifiedFields - == zenDeviceEffectsUserModifiedFields - && Objects.equals(other.deletionInstant, deletionInstant); - - if (Flags.modesUi()) { + && other.disabledOrigin == disabledOrigin + && other.legacySuppressedEffects == legacySuppressedEffects + && other.conditionOverride == conditionOverride; + if (Flags.modesCleanupImplicit()) { finalEquals = finalEquals - && other.disabledOrigin == disabledOrigin - && other.legacySuppressedEffects == legacySuppressedEffects - && other.conditionOverride == conditionOverride; + && Objects.equals(other.lastActivation, lastActivation); } } @@ -2936,26 +2897,32 @@ public class ZenModeConfig implements Parcelable { @Override public int hashCode() { - if (Flags.modesApi()) { - if (Flags.modesUi()) { + if (Flags.modesUi()) { + if (Flags.modesCleanupImplicit()) { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, - zenDeviceEffects, modified, allowManualInvocation, iconResName, + zenDeviceEffects, allowManualInvocation, iconResName, triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, deletionInstant, disabledOrigin, legacySuppressedEffects, - conditionOverride); + conditionOverride, lastActivation); } else { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, - zenDeviceEffects, modified, allowManualInvocation, iconResName, + zenDeviceEffects, allowManualInvocation, iconResName, triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, - deletionInstant); + deletionInstant, disabledOrigin, legacySuppressedEffects, + conditionOverride); } + } else { + return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, + component, configurationActivity, pkg, id, enabler, zenPolicy, + zenDeviceEffects, allowManualInvocation, iconResName, + triggerDescription, type, userModifiedFields, + zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields, + deletionInstant); } - return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, - component, configurationActivity, pkg, id, enabler, zenPolicy, modified); } /** Returns a deep copy of the {@link ZenRule}. */ @@ -2971,7 +2938,7 @@ public class ZenModeConfig implements Parcelable { } public boolean isActive() { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { if (!enabled || getPkg() == null) { return false; } else if (conditionOverride == OVERRIDE_ACTIVATE) { @@ -2989,7 +2956,7 @@ public class ZenModeConfig implements Parcelable { @VisibleForTesting(otherwise = VisibleForTesting.NONE) @ConditionOverride public int getConditionOverride() { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { return conditionOverride; } else { return snoozing ? OVERRIDE_DEACTIVATE : OVERRIDE_NONE; @@ -2997,7 +2964,7 @@ public class ZenModeConfig implements Parcelable { } public void setConditionOverride(@ConditionOverride int value) { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { conditionOverride = value; } else { if (value == OVERRIDE_ACTIVATE) { @@ -3026,7 +2993,7 @@ public class ZenModeConfig implements Parcelable { * manual deactivation (which used to be called "snoozing"). */ public void reconsiderConditionOverride() { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { if (conditionOverride == OVERRIDE_ACTIVATE && isTrueOrUnknown()) { resetConditionOverride(); } else if (conditionOverride == OVERRIDE_DEACTIVATE && !isTrueOrUnknown()) { @@ -3085,11 +3052,8 @@ public class ZenModeConfig implements Parcelable { & NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) != 0; boolean allowConversations = (policy.priorityConversationSenders & Policy.PRIORITY_CATEGORY_CONVERSATIONS) != 0; - boolean areChannelsBypassingDnd = (policy.state & Policy.STATE_CHANNELS_BYPASSING_DND) != 0; - if (Flags.modesApi()) { - areChannelsBypassingDnd = policy.hasPriorityChannels() - && policy.allowPriorityChannels(); - } + boolean areChannelsBypassingDnd = + policy.hasPriorityChannels() && policy.allowPriorityChannels(); boolean allowSystem = (policy.priorityCategories & Policy.PRIORITY_CATEGORY_SYSTEM) != 0; return !allowReminders && !allowCalls && !allowMessages && !allowEvents && !allowRepeatCallers && !areChannelsBypassingDnd && !allowSystem @@ -3129,15 +3093,12 @@ public class ZenModeConfig implements Parcelable { && !policy.isCategoryAllowed(PRIORITY_CATEGORY_EVENTS, false) && !policy.isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false) && !policy.isCategoryAllowed(PRIORITY_CATEGORY_SYSTEM, false) - && !(config.areChannelsBypassingDnd && policy.getPriorityChannelsAllowed() + && !(config.hasPriorityChannels && policy.getPriorityChannelsAllowed() == STATE_ALLOW); } else { - boolean areChannelsBypassingDnd = config.areChannelsBypassingDnd; - if (Flags.modesApi()) { - areChannelsBypassingDnd = config.areChannelsBypassingDnd - && config.isAllowPriorityChannels(); - } + boolean areChannelsBypassingDnd = config.hasPriorityChannels + && config.isAllowPriorityChannels(); return !config.isAllowReminders() && !config.isAllowCalls() && !config.isAllowMessages() && !config.isAllowEvents() && !config.isAllowRepeatCallers() && !areChannelsBypassingDnd && !config.isAllowSystem(); diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java index 31acd248dcc0..c159e4016095 100644 --- a/core/java/android/service/notification/ZenModeDiff.java +++ b/core/java/android/service/notification/ZenModeDiff.java @@ -16,7 +16,6 @@ package android.service.notification; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.Nullable; import android.app.Flags; @@ -249,7 +248,7 @@ public class ZenModeDiff { public static final String FIELD_ALLOW_MESSAGES_FROM = "allowMessagesFrom"; public static final String FIELD_ALLOW_CONVERSATIONS_FROM = "allowConversationsFrom"; public static final String FIELD_SUPPRESSED_VISUAL_EFFECTS = "suppressedVisualEffects"; - public static final String FIELD_ARE_CHANNELS_BYPASSING_DND = "areChannelsBypassingDnd"; + public static final String FIELD_HAS_PRIORITY_CHANNELS = "hasPriorityChannels"; public static final String FIELD_ALLOW_PRIORITY_CHANNELS = "allowPriorityChannels"; private static final Set<String> PEOPLE_TYPE_FIELDS = Set.of(FIELD_ALLOW_CALLS_FROM, FIELD_ALLOW_MESSAGES_FROM); @@ -323,15 +322,13 @@ public class ZenModeDiff { addField(FIELD_SUPPRESSED_VISUAL_EFFECTS, new FieldDiff<>(from.suppressedVisualEffects, to.suppressedVisualEffects)); } - if (from.areChannelsBypassingDnd != to.areChannelsBypassingDnd) { - addField(FIELD_ARE_CHANNELS_BYPASSING_DND, - new FieldDiff<>(from.areChannelsBypassingDnd, to.areChannelsBypassingDnd)); + if (from.hasPriorityChannels != to.hasPriorityChannels) { + addField(FIELD_HAS_PRIORITY_CHANNELS, + new FieldDiff<>(from.hasPriorityChannels, to.hasPriorityChannels)); } - if (Flags.modesApi()) { - if (from.allowPriorityChannels != to.allowPriorityChannels) { - addField(FIELD_ALLOW_PRIORITY_CHANNELS, - new FieldDiff<>(from.allowPriorityChannels, to.allowPriorityChannels)); - } + if (from.allowPriorityChannels != to.allowPriorityChannels) { + addField(FIELD_ALLOW_PRIORITY_CHANNELS, + new FieldDiff<>(from.allowPriorityChannels, to.allowPriorityChannels)); } // Compare automatic and manual rules @@ -491,7 +488,6 @@ public class ZenModeDiff { public static final String FIELD_ENABLER = "enabler"; public static final String FIELD_ZEN_POLICY = "zenPolicy"; public static final String FIELD_ZEN_DEVICE_EFFECTS = "zenDeviceEffects"; - public static final String FIELD_MODIFIED = "modified"; public static final String FIELD_PKG = "pkg"; public static final String FIELD_ALLOW_MANUAL = "allowManualInvocation"; public static final String FIELD_ICON_RES = "iconResName"; @@ -532,7 +528,7 @@ public class ZenModeDiff { if (from.enabled != to.enabled) { addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled)); } - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { if (from.conditionOverride != to.conditionOverride) { addField(FIELD_CONDITION_OVERRIDE, new FieldDiff<>(from.conditionOverride, to.conditionOverride)); @@ -572,51 +568,40 @@ public class ZenModeDiff { if (!Objects.equals(from.enabler, to.enabler)) { addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler)); } - if (android.app.Flags.modesApi()) { - PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy); - if (policyDiff.hasDiff()) { - addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy, - policyDiff)); - } - } else { - if (!Objects.equals(from.zenPolicy, to.zenPolicy)) { - addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy)); - } - } - if (from.modified != to.modified) { - addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified)); + PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy); + if (policyDiff.hasDiff()) { + addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy, + policyDiff)); } if (!Objects.equals(from.pkg, to.pkg)) { addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg)); } - if (android.app.Flags.modesApi()) { - DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects, - to.zenDeviceEffects); - if (deviceEffectsDiff.hasDiff()) { - addField(FIELD_ZEN_DEVICE_EFFECTS, - new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects, - deviceEffectsDiff)); - } - if (!Objects.equals(from.triggerDescription, to.triggerDescription)) { - addField(FIELD_TRIGGER_DESCRIPTION, - new FieldDiff<>(from.triggerDescription, to.triggerDescription)); - } - if (from.type != to.type) { - addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type)); - } - if (from.allowManualInvocation != to.allowManualInvocation) { - addField(FIELD_ALLOW_MANUAL, - new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation)); - } - if (!Objects.equals(from.iconResName, to.iconResName)) { - addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName)); - } - if (android.app.Flags.modesUi()) { - if (from.legacySuppressedEffects != to.legacySuppressedEffects) { - addField(FIELD_LEGACY_SUPPRESSED_EFFECTS, - new FieldDiff<>(from.legacySuppressedEffects, - to.legacySuppressedEffects)); - } + DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects, + to.zenDeviceEffects); + if (deviceEffectsDiff.hasDiff()) { + addField(FIELD_ZEN_DEVICE_EFFECTS, + new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects, + deviceEffectsDiff)); + } + if (!Objects.equals(from.triggerDescription, to.triggerDescription)) { + addField(FIELD_TRIGGER_DESCRIPTION, + new FieldDiff<>(from.triggerDescription, to.triggerDescription)); + } + if (from.type != to.type) { + addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type)); + } + if (from.allowManualInvocation != to.allowManualInvocation) { + addField(FIELD_ALLOW_MANUAL, + new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation)); + } + if (!Objects.equals(from.iconResName, to.iconResName)) { + addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName)); + } + if (android.app.Flags.modesUi()) { + if (from.legacySuppressedEffects != to.legacySuppressedEffects) { + addField(FIELD_LEGACY_SUPPRESSED_EFFECTS, + new FieldDiff<>(from.legacySuppressedEffects, + to.legacySuppressedEffects)); } } } @@ -702,7 +687,6 @@ public class ZenModeDiff { * Diff class representing a change between two * {@link android.service.notification.ZenDeviceEffects}. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static class DeviceEffectsDiff extends BaseDiff { public static final String FIELD_GRAYSCALE = "mGrayscale"; public static final String FIELD_SUPPRESS_AMBIENT_DISPLAY = "mSuppressAmbientDisplay"; @@ -843,7 +827,6 @@ public class ZenModeDiff { /** * Diff class representing a change between two {@link android.service.notification.ZenPolicy}. */ - @FlaggedApi(Flags.FLAG_MODES_API) public static class PolicyDiff extends BaseDiff { public static final String FIELD_PRIORITY_CATEGORY_REMINDERS = "mPriorityCategories_Reminders"; diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index 4cff67e24a0f..6b98c4144f91 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -16,13 +16,11 @@ package android.service.notification; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.TestApi; -import android.app.Flags; import android.app.Notification; import android.app.NotificationChannel; import android.os.Parcel; @@ -78,91 +76,74 @@ public final class ZenPolicy implements Parcelable { * the same time. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_MESSAGES = 1 << 0; /** * Covers modifications to CALL_SENDERS and PRIORITY_CATEGORY_CALLS, which are set at * the same time. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_CALLS = 1 << 1; /** * Covers modifications to CONVERSATION_SENDERS and PRIORITY_CATEGORY_CONVERSATIONS, which are * set at the same time. * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_CONVERSATIONS = 1 << 2; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_ALLOW_CHANNELS = 1 << 3; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_REMINDERS = 1 << 4; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15; /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16; private List<Integer> mPriorityCategories; @@ -170,7 +151,6 @@ public final class ZenPolicy implements Parcelable { private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET; private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET; private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET; - @FlaggedApi(Flags.FLAG_MODES_API) private @ChannelType int mAllowChannels = CHANNEL_POLICY_UNSET; /** @hide */ @@ -358,7 +338,6 @@ public final class ZenPolicy implements Parcelable { * * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int CHANNEL_POLICY_UNSET = 0; /** @@ -367,7 +346,6 @@ public final class ZenPolicy implements Parcelable { * * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int CHANNEL_POLICY_PRIORITY = 1; /** @@ -376,7 +354,6 @@ public final class ZenPolicy implements Parcelable { * * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static final int CHANNEL_POLICY_NONE = 2; /** @hide */ @@ -386,7 +363,6 @@ public final class ZenPolicy implements Parcelable { } /** @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public ZenPolicy(List<Integer> priorityCategories, List<Integer> visualEffects, @PeopleType int priorityMessages, @PeopleType int priorityCalls, @ConversationSenders int conversationSenders, @ChannelType int allowChannels) { @@ -409,7 +385,6 @@ public final class ZenPolicy implements Parcelable { * * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static ZenPolicy getBasePolicyInterruptionFilterAlarms() { return new ZenPolicy.Builder() .disallowAllSounds() @@ -430,7 +405,6 @@ public final class ZenPolicy implements Parcelable { * * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static ZenPolicy getBasePolicyInterruptionFilterNone() { return new ZenPolicy.Builder() .disallowAllSounds() @@ -628,7 +602,6 @@ public final class ZenPolicy implements Parcelable { * channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels * with {@link NotificationChannel#canBypassDnd()} will be intercepted. */ - @FlaggedApi(Flags.FLAG_MODES_API) public @State int getPriorityChannelsAllowed() { switch (mAllowChannels) { case CHANNEL_POLICY_PRIORITY: @@ -695,14 +668,10 @@ public final class ZenPolicy implements Parcelable { * Builds the current ZenPolicy. */ public @NonNull ZenPolicy build() { - if (Flags.modesApi()) { - return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories), - new ArrayList<>(mZenPolicy.mVisualEffects), - mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls, - mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels); - } else { - return mZenPolicy.copy(); - } + return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories), + new ArrayList<>(mZenPolicy.mVisualEffects), + mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls, + mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels); } /** @@ -1054,7 +1023,6 @@ public final class ZenPolicy implements Parcelable { * Set whether priority channels are permitted to break through DND. */ @SuppressLint("BuilderSetStyle") - @FlaggedApi(Flags.FLAG_MODES_API) public @NonNull Builder allowPriorityChannels(boolean allow) { mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE; return this; @@ -1079,38 +1047,21 @@ public final class ZenPolicy implements Parcelable { dest.writeInt(mPriorityMessages); dest.writeInt(mPriorityCalls); dest.writeInt(mConversationSenders); - if (Flags.modesApi()) { - dest.writeInt(mAllowChannels); - } + dest.writeInt(mAllowChannels); } public static final @NonNull Creator<ZenPolicy> CREATOR = new Creator<ZenPolicy>() { @Override public ZenPolicy createFromParcel(Parcel source) { - ZenPolicy policy; - if (Flags.modesApi()) { - policy = new ZenPolicy( - trimList(source.readArrayList(Integer.class.getClassLoader(), - Integer.class), NUM_PRIORITY_CATEGORIES), - trimList(source.readArrayList(Integer.class.getClassLoader(), - Integer.class), NUM_VISUAL_EFFECTS), - source.readInt(), source.readInt(), source.readInt(), - source.readInt() + return new ZenPolicy( + trimList(source.readArrayList(Integer.class.getClassLoader(), + Integer.class), NUM_PRIORITY_CATEGORIES), + trimList(source.readArrayList(Integer.class.getClassLoader(), + Integer.class), NUM_VISUAL_EFFECTS), + source.readInt(), source.readInt(), source.readInt(), + source.readInt() ); - } else { - policy = new ZenPolicy(); - policy.mPriorityCategories = - trimList(source.readArrayList(Integer.class.getClassLoader(), - Integer.class), NUM_PRIORITY_CATEGORIES); - policy.mVisualEffects = - trimList(source.readArrayList(Integer.class.getClassLoader(), - Integer.class), NUM_VISUAL_EFFECTS); - policy.mPriorityMessages = source.readInt(); - policy.mPriorityCalls = source.readInt(); - policy.mConversationSenders = source.readInt(); - } - return policy; } @Override @@ -1121,18 +1072,16 @@ public final class ZenPolicy implements Parcelable { @Override public String toString() { - StringBuilder sb = new StringBuilder(ZenPolicy.class.getSimpleName()) + return new StringBuilder(ZenPolicy.class.getSimpleName()) .append('{') .append("priorityCategories=[").append(priorityCategoriesToString()) .append("], visualEffects=[").append(visualEffectsToString()) .append("], priorityCallsSenders=").append(peopleTypeToString(mPriorityCalls)) .append(", priorityMessagesSenders=").append(peopleTypeToString(mPriorityMessages)) .append(", priorityConversationSenders=").append( - conversationTypeToString(mConversationSenders)); - if (Flags.modesApi()) { - sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels)); - } - return sb.append('}').toString(); + conversationTypeToString(mConversationSenders)) + .append(", allowChannels=").append(channelTypeToString(mAllowChannels)) + .append('}').toString(); } /** @hide */ @@ -1325,7 +1274,6 @@ public final class ZenPolicy implements Parcelable { /** * @hide */ - @FlaggedApi(Flags.FLAG_MODES_API) public static String channelTypeToString(@ChannelType int channelType) { switch (channelType) { case CHANNEL_POLICY_UNSET: @@ -1344,25 +1292,18 @@ public final class ZenPolicy implements Parcelable { if (o == this) return true; final ZenPolicy other = (ZenPolicy) o; - boolean eq = Objects.equals(other.mPriorityCategories, mPriorityCategories) + return Objects.equals(other.mPriorityCategories, mPriorityCategories) && Objects.equals(other.mVisualEffects, mVisualEffects) && other.mPriorityCalls == mPriorityCalls && other.mPriorityMessages == mPriorityMessages - && other.mConversationSenders == mConversationSenders; - if (Flags.modesApi()) { - return eq && other.mAllowChannels == mAllowChannels; - } - return eq; + && other.mConversationSenders == mConversationSenders + && other.mAllowChannels == mAllowChannels; } @Override public int hashCode() { - if (Flags.modesApi()) { - return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, - mPriorityMessages, mConversationSenders, mAllowChannels); - } - return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages, - mConversationSenders); + return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, + mPriorityMessages, mConversationSenders, mAllowChannels); } private @State int getZenPolicyPriorityCategoryState(@PriorityCategory int @@ -1480,13 +1421,10 @@ public final class ZenPolicy implements Parcelable { } } - // apply allowed channels - if (Flags.modesApi()) { - // if no channels are allowed, can't newly allow them - if (mAllowChannels != CHANNEL_POLICY_NONE - && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) { - mAllowChannels = policyToApply.mAllowChannels; - } + // apply allowed channels -> if no channels are allowed, can't newly allow them + if (mAllowChannels != CHANNEL_POLICY_NONE + && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) { + mAllowChannels = policyToApply.mAllowChannels; } } @@ -1499,7 +1437,6 @@ public final class ZenPolicy implements Parcelable { * @hide */ @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) public @NonNull ZenPolicy overwrittenWith(@Nullable ZenPolicy newPolicy) { ZenPolicy result = this.copy(); @@ -1596,10 +1533,7 @@ public final class ZenPolicy implements Parcelable { proto.write(DNDPolicyProto.ALLOW_CALLS_FROM, getPriorityCallSenders()); proto.write(DNDPolicyProto.ALLOW_MESSAGES_FROM, getPriorityMessageSenders()); proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders()); - - if (Flags.modesApi()) { - proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed()); - } + proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed()); proto.flush(); return bytes.toByteArray(); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 464756842caf..41a64e22e058 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -1595,8 +1595,17 @@ public abstract class WallpaperService extends Service { mWindow.setSession(mSession); mLayout.packageName = getPackageName(); - mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener, - mCaller.getHandler()); + if (com.android.server.display.feature.flags.Flags + .displayListenerPerformanceImprovements() + && com.android.server.display.feature.flags.Flags + .committedStateSeparateEvent()) { + mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener, + mCaller.getHandler(), DisplayManager.EVENT_TYPE_DISPLAY_CHANGED, + DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED); + } else { + mIWallpaperEngine.mDisplayManager.registerDisplayListener(mDisplayListener, + mCaller.getHandler()); + } mDisplay = mIWallpaperEngine.mDisplay; // Use window context of TYPE_WALLPAPER so client can access UI resources correctly. mDisplayContext = createDisplayContext(mDisplay) diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index ca0959af3ff8..231aa6816908 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -1599,7 +1599,6 @@ public final class Display { mGlobal.registerDisplayListener(toRegister, executor, DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, ActivityThread.currentPackageName()); diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index ecdbaa3cd2f4..d880072aa404 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -44,6 +44,7 @@ import android.util.SparseArray; import android.util.proto.ProtoOutputStream; import com.android.internal.display.BrightnessSynchronizer; +import com.android.server.display.feature.flags.Flags; import java.util.Arrays; import java.util.Objects; @@ -447,18 +448,20 @@ public final class DisplayInfo implements Parcelable { } public boolean equals(DisplayInfo other) { - return equals(other, /* compareRefreshRate */ true); + return equals(other, /* compareOnlyBasicChanges */ false); } /** * Compares if the two DisplayInfo objects are equal or not * @param other The other DisplayInfo against which the comparison is to be done - * @param compareRefreshRate Indicates if the refresh rate is also to be considered in - * comparison + * @param compareOnlyBasicChanges Indicates if the changes to be compared are the ones which + * could lead to an emission of + * {@link android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED} + * event * @return */ - public boolean equals(DisplayInfo other, boolean compareRefreshRate) { - boolean isEqualWithoutRefreshRate = other != null + public boolean equals(DisplayInfo other, boolean compareOnlyBasicChanges) { + boolean isEqualWithOnlyBasicChanges = other != null && layerStack == other.layerStack && flags == other.flags && type == other.type @@ -494,7 +497,6 @@ public final class DisplayInfo implements Parcelable { && physicalXDpi == other.physicalXDpi && physicalYDpi == other.physicalYDpi && state == other.state - && committedState == other.committedState && ownerUid == other.ownerUid && Objects.equals(ownerPackageName, other.ownerPackageName) && removeMode == other.removeMode @@ -512,14 +514,19 @@ public final class DisplayInfo implements Parcelable { thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId) && canHostTasks == other.canHostTasks; - if (compareRefreshRate) { - return isEqualWithoutRefreshRate + if (!Flags.committedStateSeparateEvent()) { + isEqualWithOnlyBasicChanges = isEqualWithOnlyBasicChanges + && (committedState == other.committedState); + } + if (!compareOnlyBasicChanges) { + return isEqualWithOnlyBasicChanges && (getRefreshRate() == other.getRefreshRate()) && appVsyncOffsetNanos == other.appVsyncOffsetNanos && presentationDeadlineNanos == other.presentationDeadlineNanos - && (modeId == other.modeId); + && (modeId == other.modeId) + && (committedState == other.committedState); } - return isEqualWithoutRefreshRate; + return isEqualWithOnlyBasicChanges; } @Override diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java index 73cd5ecd39ef..df680c054f56 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -32,7 +32,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; -import android.util.TypedValue; import android.widget.FrameLayout; import android.widget.RelativeLayout; import android.widget.RemoteViews; @@ -266,20 +265,14 @@ public class NotificationHeaderView extends RelativeLayout { ? R.style.TextAppearance_DeviceDefault_Notification_Title : R.style.TextAppearance_DeviceDefault_Notification_Info; // Most of the time, we're showing text in the minimized state - if (findViewById(R.id.header_text) instanceof TextView headerText) { - headerText.setTextAppearance(styleResId); - if (notificationsRedesignTemplates()) { - // TODO: b/378660052 - When inlining the redesign flag, this should be updated - // directly in TextAppearance_DeviceDefault_Notification_Title so we won't need to - // override it here. - float textSize = getContext().getResources().getDimension( - R.dimen.notification_2025_title_text_size); - headerText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); - } + View headerText = findViewById(R.id.header_text); + if (headerText instanceof TextView) { + ((TextView) headerText).setTextAppearance(styleResId); } // If there's no summary or text, we show the app name instead of nothing - if (findViewById(R.id.app_name_text) instanceof TextView appNameText) { - appNameText.setTextAppearance(styleResId); + View appNameText = findViewById(R.id.app_name_text); + if (appNameText instanceof TextView) { + ((TextView) appNameText).setTextAppearance(styleResId); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 80b4f2caabbb..6b6147a3749d 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -118,6 +118,7 @@ import static android.view.flags.Flags.addSchandleToVriSurface; import static android.view.flags.Flags.disableDrawWakeLock; import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.view.flags.Flags.sensitiveContentPrematureProtectionRemovedFix; +import static android.view.flags.Flags.toolkitFrameRateDebug; import static android.view.flags.Flags.toolkitFrameRateFunctionEnablingReadOnly; import static android.view.flags.Flags.toolkitFrameRateTouchBoost25q1; import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly; @@ -538,6 +539,11 @@ public final class ViewRootImpl implements ViewParent, private static boolean sAlwaysAssignFocus; /** + * whether we pre-initialized the Buffer Allocator + */ + private static boolean sPreInitializedBufferAllocator = false; + + /** * This list must only be modified by the main thread. */ final ArrayList<WindowCallbacks> mWindowCallbacks = new ArrayList<>(); @@ -1222,6 +1228,7 @@ public final class ViewRootImpl implements ViewParent, com.android.graphics.surfaceflinger.flags.Flags.vrrBugfix24q4(); private static final boolean sEnableVrr = ViewProperties.vrr_enabled().orElse(true); private static final boolean sToolkitInitialTouchBoostFlagValue = toolkitInitialTouchBoost(); + private static boolean sToolkitFrameRateDebugFlagValue = toolkitFrameRateDebug(); static { sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly(); @@ -1342,6 +1349,11 @@ public final class ViewRootImpl implements ViewParent, com.android.server.display.feature.flags.Flags.subscribeGranularDisplayEvents(); mSendPerfHintOnTouch = adpfViewrootimplActionDownBoost(); + + if (!sPreInitializedBufferAllocator) { + preInitBufferAllocator(); + sPreInitializedBufferAllocator = true; + } } public static void addFirstDrawHandler(Runnable callback) { @@ -13129,6 +13141,11 @@ public final class ViewRootImpl implements ViewParent, if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) { mFrameRateTransaction.setFrameRateCategory(mSurfaceControl, frameRateCategory, false).applyAsyncUnsafe(); + + if (sToolkitFrameRateDebugFlagValue) { + Log.v(mTag, "### ViewRootImpl setFrameRateCategory '" + + categoryToString(frameRateCategory) + "'"); + } } mLastPreferredFrameRateCategory = frameRateCategory; } @@ -13191,8 +13208,15 @@ public final class ViewRootImpl implements ViewParent, if (preferredFrameRate > 0) { mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate, mFrameRateCompatibility); + if (sToolkitFrameRateDebugFlagValue) { + Log.v(mTag, "### ViewRootImpl setFrameRate '" + + preferredFrameRate + "'"); + } } else { mFrameRateTransaction.clearFrameRate(mSurfaceControl); + if (sToolkitFrameRateDebugFlagValue) { + Log.v(mTag, "### ViewRootImpl setFrameRate 0 Hz"); + } } mFrameRateTransaction.applyAsyncUnsafe(); } @@ -13246,6 +13270,12 @@ public final class ViewRootImpl implements ViewParent, // mFrameRateCategoryView = view == null ? "-" : view.getClass().getSimpleName(); } mDrawnThisFrame = true; + + if (sToolkitFrameRateDebugFlagValue) { + String viewName = view == null ? "-" : view.getClass().getSimpleName(); + Log.v(mTag, "### View: " + viewName + " votes '" + + categoryToString(frameRateCategory) + "'"); + } } /** @@ -13562,4 +13592,10 @@ public final class ViewRootImpl implements ViewParent, sProtoLogInitialized = true; } } + + private void preInitBufferAllocator() { + if (com.android.graphics.hwui.flags.Flags.earlyPreinitBufferAllocator()) { + ThreadedRenderer.preInitBufferAllocator(); + } + } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index db699d7bfb06..93eed370004b 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -625,12 +625,6 @@ public interface WindowManager extends ViewManager { int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000 /** - * Transition flag: Indicates that aod is showing hidden by entering doze - * @hide - */ - int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000 - - /** * @hide */ @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = { @@ -649,7 +643,6 @@ public interface WindowManager extends ViewManager { TRANSIT_FLAG_KEYGUARD_OCCLUDING, TRANSIT_FLAG_KEYGUARD_UNOCCLUDING, TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH, - TRANSIT_FLAG_AOD_APPEARING, }) @Retention(RetentionPolicy.SOURCE) @interface TransitionFlags {} @@ -666,8 +659,7 @@ public interface WindowManager extends ViewManager { (TRANSIT_FLAG_KEYGUARD_GOING_AWAY | TRANSIT_FLAG_KEYGUARD_APPEARING | TRANSIT_FLAG_KEYGUARD_OCCLUDING - | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING - | TRANSIT_FLAG_AOD_APPEARING); + | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING); /** * Remove content mode: Indicates remove content mode is currently not defined. diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 578b7b6a63fa..ede0b3cf8cce 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -2684,9 +2684,10 @@ public class AccessibilityNodeInfo implements Parcelable { * <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection} * should be constructed with {@code this} node or a descendant of it. * - * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and {@link - * AccessibilityNodeInfo#setFocused} should both be called with {@code true} before setting the - * selection in order to make {@code this} node a candidate to contain a selection. + * <p><b>Note:</b> {@link AccessibilityNodeInfo#setFocusable} and + * {@link AccessibilityNodeInfo#setFocused} should both be called with {@code true} + * before setting the selection in order to make {@code this} node a candidate to + * contain a selection. * * <p><b>Note:</b> Cannot be called from an AccessibilityService. This class is made immutable * before being delivered to an AccessibilityService. @@ -2706,12 +2707,10 @@ public class AccessibilityNodeInfo implements Parcelable { * Gets the extended selection, which is a representation of selection that spans multiple nodes * that exist within the subtree of the node defining selection. * - * <p><b>Note:</b> The start and end {@link SelectionPosition} of the provided {@link Selection} - * should be constructed with {@code this} node or a descendant of it. - * - * <p><b>Note:</b> In order for a node to be a candidate to contain a selection, {@link - * AccessibilityNodeInfo#isFocusable()} ()} and {@link AccessibilityNodeInfo#isFocused()} should - * both be return with {@code true}. + * <p><b>Note:</b> Nodes that are candidates to contain a selection should return + * {@code true} from {@link #isFocusable()} and {@link #isFocused()}. + * The start and end {@link SelectionPosition}s of this {@link Selection} + * should exist within {@code this} node or its descendants. * * @return The extended selection within the node's subtree, or {@code null} if no selection * exists. @@ -5840,8 +5839,8 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Instantiates a new SelectionPosition. * - * @param view The {@link View} containing the virtual descendant associated with the - * selection position. + * @param view The {@link View} containing the text associated with this selection + * position. * @param offset The offset for a selection position within {@code view}'s text content, * which should be a value between 0 and the length of {@code view}'s text. */ diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index 3bc2205f8e1c..18fa0f353f36 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -143,4 +143,11 @@ flag { namespace: "toolkit" description: "Feature flag to update initial touch boost logic" bug: "393004744" +} + +flag { + name: "toolkit_frame_rate_debug" + namespace: "toolkit" + description: "Feature flag to ennable ARR debug message" + bug: "394614443" }
\ No newline at end of file diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 0f5476f58f74..0a5c14e3a08b 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -8565,12 +8565,16 @@ public class RemoteViews implements Parcelable, Filter { return context; } try { - // Use PackageManager as the source of truth for application information, rather - // than the parceled ApplicationInfo provided by the app. - ApplicationInfo sanitizedApplication = - context.getPackageManager().getApplicationInfoAsUser( - mApplication.packageName, 0, - UserHandle.getUserId(mApplication.uid)); + ApplicationInfo sanitizedApplication = mApplication; + try { + // Use PackageManager as the source of truth for application information, rather + // than the parceled ApplicationInfo provided by the app. + sanitizedApplication = context.getPackageManager().getApplicationInfoAsUser( + mApplication.packageName, 0, UserHandle.getUserId(mApplication.uid)); + } catch(SecurityException se) { + Log.d(LOG_TAG, "Unable to fetch appInfo for " + mApplication.packageName); + } + Context applicationContext = context.createApplicationContext( sanitizedApplication, Context.CONTEXT_RESTRICTED); diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index d43469fa76ca..ca1017b72854 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -96,6 +96,7 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true), ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true), + ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false), ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true), ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true), ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true), diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index cf21e50e0a19..4f34aa36a204 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -29,7 +29,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_NONE; @@ -406,8 +405,7 @@ public final class TransitionInfo implements Parcelable { */ public boolean hasChangesOrSideEffects() { return !mChanges.isEmpty() || isKeyguardGoingAway() - || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0 - || (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0; + || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0; } /** diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 3266ad4d93ae..79120b22c205 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -79,6 +79,16 @@ flag { } flag { + name: "enable_drag_resize_set_up_in_bg_thread" + namespace: "lse_desktop_experience" + description: "Enables setting up the drag-resize input listener in a bg thread" + bug: "396445663" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_desktop_windowing_wallpaper_activity" namespace: "lse_desktop_experience" description: "Enables desktop wallpaper activity to show wallpaper in the desktop mode" @@ -165,6 +175,16 @@ flag { } flag { + name: "enable_camera_compat_track_task_and_app_bugfix" + namespace: "lse_desktop_experience" + description: "Whether to use taskId and app process to track camera apps, and notify the policies only on first camera open and final close" + bug: "380840084" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_task_stack_observer_in_shell" namespace: "lse_desktop_experience" description: "Introduces a new observer in shell to track the task stack." diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index 21d000dc5224..b57acf3d97fd 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -226,6 +226,21 @@ public class AconfigFlags { } private Boolean getFlagValueFromNewStorage(String flagPackageAndName) { + // We still need to check mFlagValues in case addFlagValuesForTesting() was called for + // testing purposes. + if (!mFlagValues.isEmpty() && mFlagValues.containsKey(flagPackageAndName)) { + Boolean value = mFlagValues.get(flagPackageAndName); + if (DEBUG) { + Slog.v( + LOG_TAG, + "Aconfig flag value (FOR TESTING) for " + + flagPackageAndName + + " = " + + value); + } + return value; + } + int index = flagPackageAndName.lastIndexOf('.'); if (index < 0) { Slog.e(LOG_TAG, "Unable to parse package name from " + flagPackageAndName); diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index 37500766a4ac..ac186d0a26b5 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -56,8 +56,7 @@ public abstract class VerityUtils { private static final int HASH_SIZE_BYTES = 32; public static boolean isFsVeritySupported() { - return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R - || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; + return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R; } /** Enables fs-verity for the file without signature. */ diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index 0ec55f958f38..1f907602cb9b 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -87,10 +87,10 @@ public class LockPatternView extends View { private static final int CELL_ACTIVATE = 0; private static final int CELL_DEACTIVATE = 1; - private final int mDotSize; - private final int mDotSizeActivated; + private int mDotSize; + private int mDotSizeActivated; private final float mDotHitFactor; - private final int mPathWidth; + private int mPathWidth; private final int mLineFadeOutAnimationDurationMs; private final int mLineFadeOutAnimationDelayMs; private final int mFadePatternAnimationDurationMs; @@ -1341,6 +1341,38 @@ public class LockPatternView extends View { invalidate(); } + /** + * Change dot colors + */ + public void setDotColors(int dotColor, int dotActivatedColor) { + mDotColor = dotColor; + mDotActivatedColor = dotActivatedColor; + invalidate(); + } + + /** + * Keeps dot activated until the next dot gets activated. + */ + public void setKeepDotActivated(boolean keepDotActivated) { + mKeepDotActivated = keepDotActivated; + } + + /** + * Set dot sizes in dp + */ + public void setDotSizes(int dotSizeDp, int dotSizeActivatedDp) { + mDotSize = dotSizeDp; + mDotSizeActivated = dotSizeActivatedDp; + } + + /** + * Set the stroke width of the pattern line. + */ + public void setPathWidth(int pathWidthDp) { + mPathWidth = pathWidthDp; + mPathPaint.setStrokeWidth(mPathWidth); + } + private float getCenterXForColumn(int column) { return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; } diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java index ca355c41f7a9..b8503da2c09b 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java +++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java @@ -65,7 +65,7 @@ public class CoreDocument implements Serializable { // We also keep a more fine-grained BUILD number, exposed as // ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD - static final float BUILD = 0.1f; + static final float BUILD = 0.2f; @NonNull ArrayList<Operation> mOperations = new ArrayList<>(); @@ -742,6 +742,7 @@ public class CoreDocument implements Serializable { if (op instanceof Component) { mComponentMap.put(((Component) op).getComponentId(), (Component) op); registerVariables(context, ((Component) op).getList()); + ((Component) op).registerVariables(context); } if (op instanceof ComponentValue) { ComponentValue v = (ComponentValue) op; diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java index 9bb8d9f39975..09ec40271f4d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java +++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java @@ -35,6 +35,7 @@ import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontT import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled; import com.android.internal.widget.remotecompose.core.operations.DrawCircle; +import com.android.internal.widget.remotecompose.core.operations.DrawContent; import com.android.internal.widget.remotecompose.core.operations.DrawLine; import com.android.internal.widget.remotecompose.core.operations.DrawOval; import com.android.internal.widget.remotecompose.core.operations.DrawPath; @@ -81,6 +82,7 @@ import com.android.internal.widget.remotecompose.core.operations.Theme; import com.android.internal.widget.remotecompose.core.operations.TimeAttribute; import com.android.internal.widget.remotecompose.core.operations.TouchExpression; import com.android.internal.widget.remotecompose.core.operations.layout.CanvasContent; +import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations; import com.android.internal.widget.remotecompose.core.operations.layout.ClickModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart; import com.android.internal.widget.remotecompose.core.operations.layout.ContainerEnd; @@ -105,6 +107,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.modifier import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentVisibilityOperation; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DrawContentOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.GraphicsLayerModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightInModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation; @@ -172,6 +175,7 @@ public class Operations { public static final int DATA_PATH = 123; public static final int DRAW_PATH = 124; public static final int DRAW_TWEEN_PATH = 125; + public static final int DRAW_CONTENT = 139; public static final int MATRIX_SCALE = 126; public static final int MATRIX_TRANSLATE = 127; public static final int MATRIX_SKEW = 128; @@ -215,6 +219,8 @@ public class Operations { public static final int ATTRIBUTE_TEXT = 170; public static final int ATTRIBUTE_IMAGE = 171; public static final int ATTRIBUTE_TIME = 172; + public static final int CANVAS_OPERATIONS = 173; + public static final int MODIFIER_DRAW_CONTENT = 174; ///////////////////////////////////////// ====================== @@ -366,6 +372,7 @@ public class Operations { map.put(MODIFIER_SCROLL, ScrollModifierOperation::read); map.put(MODIFIER_MARQUEE, MarqueeModifierOperation::read); map.put(MODIFIER_RIPPLE, RippleModifierOperation::read); + map.put(MODIFIER_DRAW_CONTENT, DrawContentOperation::read); map.put(CONTAINER_END, ContainerEnd::read); @@ -393,6 +400,7 @@ public class Operations { map.put(LAYOUT_TEXT, TextLayout::read); map.put(LAYOUT_STATE, StateLayout::read); + map.put(DRAW_CONTENT, DrawContent::read); map.put(COMPONENT_VALUE, ComponentValue::read); map.put(DRAW_ARC, DrawArc::read); @@ -409,6 +417,7 @@ public class Operations { map.put(PARTICLE_LOOP, ParticlesLoop::read); map.put(FUNCTION_CALL, FloatFunctionCall::read); map.put(FUNCTION_DEFINE, FloatFunctionDefine::read); + map.put(CANVAS_OPERATIONS, CanvasOperations::read); map.put(ACCESSIBILITY_SEMANTICS, CoreSemantics::read); map.put(ATTRIBUTE_IMAGE, ImageAttribute::read); diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java index 39f85f600310..e75bd30b381d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java @@ -38,6 +38,7 @@ import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontT import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled; import com.android.internal.widget.remotecompose.core.operations.DrawCircle; +import com.android.internal.widget.remotecompose.core.operations.DrawContent; import com.android.internal.widget.remotecompose.core.operations.DrawLine; import com.android.internal.widget.remotecompose.core.operations.DrawOval; import com.android.internal.widget.remotecompose.core.operations.DrawPath; @@ -84,6 +85,7 @@ import com.android.internal.widget.remotecompose.core.operations.TimeAttribute; import com.android.internal.widget.remotecompose.core.operations.TouchExpression; import com.android.internal.widget.remotecompose.core.operations.Utils; import com.android.internal.widget.remotecompose.core.operations.layout.CanvasContent; +import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations; import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart; import com.android.internal.widget.remotecompose.core.operations.layout.ContainerEnd; import com.android.internal.widget.remotecompose.core.operations.layout.ImpulseOperation; @@ -1887,7 +1889,7 @@ public class RemoteComposeBuffer { } /** Add a component end tag */ - public void addComponentEnd() { + public void addContainerEnd() { ContainerEnd.apply(mBuffer); } @@ -2231,6 +2233,11 @@ public class RemoteComposeBuffer { LayoutComponentContent.apply(mBuffer, mLastComponentId); } + /** Add a canvas operations start tag */ + public void addCanvasOperationsStart() { + CanvasOperations.apply(mBuffer); + } + /** * Add a component width value * @@ -2427,4 +2434,9 @@ public class RemoteComposeBuffer { TimeAttribute.apply(mBuffer, id, timeId, attribute, args); return Utils.asNan(id); } + + /** In the context of a component draw modifier, draw the content of the component */ + public void drawComponentContent() { + DrawContent.apply(mBuffer); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java new file mode 100644 index 000000000000..e2e22acbeb8f --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java @@ -0,0 +1,119 @@ +/* + * 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.internal.widget.remotecompose.core.operations; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.PaintContext; +import com.android.internal.widget.remotecompose.core.PaintOperation; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; + +import java.util.List; + +/** The DrawContent command */ +public class DrawContent extends PaintOperation implements Serializable { + private static final int OP_CODE = Operations.DRAW_CONTENT; + private static final String CLASS_NAME = "DrawContent"; + private @Nullable LayoutComponent mComponent; + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer); + } + + /** + * Set the component to be painted + * + * @param component + */ + public void setComponent(LayoutComponent component) { + mComponent = component; + } + + @NonNull + @Override + public String toString() { + return "DrawContent;"; + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + DrawContent op = new DrawContent(); + operations.add(op); + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return CLASS_NAME; + } + + /** + * The OP_CODE for this command + * + * @return the opcode + */ + public static int id() { + return OP_CODE; + } + + /** + * add a draw content operation to the buffer + * + * @param buffer the buffer to add to + */ + public static void apply(@NonNull WireBuffer buffer) { + buffer.start(Operations.DRAW_CONTENT); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Layout Operations", OP_CODE, CLASS_NAME) + .description("Draw the component content"); + } + + @Override + public void paint(@NonNull PaintContext context) { + if (mComponent != null) { + mComponent.drawContent(context); + } + } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java new file mode 100644 index 000000000000..3e7f1d304315 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java @@ -0,0 +1,175 @@ +/* + * 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.internal.widget.remotecompose.core.operations.layout; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.PaintContext; +import com.android.internal.widget.remotecompose.core.PaintOperation; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.ComponentValue; +import com.android.internal.widget.remotecompose.core.operations.DrawContent; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; + +import java.util.ArrayList; +import java.util.List; + +/** Represents a list of canvas operations. */ +public class CanvasOperations extends PaintOperation + implements VariableSupport, Container, Serializable { + private static final int OP_CODE = Operations.CANVAS_OPERATIONS; + private static final String CLASS_NAME = "CanvasOperations"; + + @NonNull public ArrayList<Operation> mList = new ArrayList<>(); + @Nullable LayoutComponent mComponent; + + /** The constructor */ + public CanvasOperations() {} + + @Override + public void registerListening(RemoteContext context) { + for (Operation operation : mList) { + if (operation instanceof VariableSupport) { + VariableSupport variableSupport = (VariableSupport) operation; + variableSupport.registerListening(context); + } + if (operation instanceof ComponentValue) { + ComponentValue v = (ComponentValue) operation; + mComponent.addComponentValue(v); + } + } + } + + @Override + public void updateVariables(RemoteContext context) { + for (Operation operation : mList) { + if (operation instanceof VariableSupport) { + VariableSupport variableSupport = (VariableSupport) operation; + variableSupport.updateVariables(context); + } + } + } + + /** + * The returns a list to be filled + * + * @return list to be filled + */ + @NonNull + public ArrayList<Operation> getList() { + return mList; + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer); + } + + @NonNull + @Override + public String toString() { + StringBuilder builder = new StringBuilder(CLASS_NAME + "\n"); + for (Operation operation : mList) { + builder.append(" "); + builder.append(operation); + builder.append("\n"); + } + return builder.toString(); + } + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return (indent != null ? indent : "") + toString(); + } + + @Override + public void paint(@NonNull PaintContext context) { + RemoteContext remoteContext = context.getContext(); + for (Operation op : mList) { + if (op instanceof VariableSupport && op.isDirty()) { + ((VariableSupport) op).updateVariables(context.getContext()); + } + remoteContext.incrementOpCount(); + op.apply(context.getContext()); + } + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return "Loop"; + } + + /** + * Apply this operation to the buffer + * + * @param buffer + */ + public static void apply(@NonNull WireBuffer buffer) { + buffer.start(OP_CODE); + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + operations.add(new CanvasOperations()); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Operations", OP_CODE, name()) + .description("Impulse Process that runs a list of operations"); + } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("list", mList); + } + + /** + * Set layout component + * + * @param layoutComponent + */ + public void setComponent(LayoutComponent layoutComponent) { + mComponent = layoutComponent; + for (Operation op : mList) { + if (op instanceof DrawContent) { + ((DrawContent) op).setComponent(layoutComponent); + } + } + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java index e332e4be4c8d..c73643682b55 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java @@ -329,6 +329,15 @@ public class Component extends PaintOperation mAnimationSpec = animationSpec; } + /** + * If the component contains variables beside mList, make sure to register them here + * + * @param context + */ + public void registerVariables(RemoteContext context) { + // Nothing here + } + public enum Visibility { GONE, VISIBLE, @@ -976,6 +985,17 @@ public class Component extends PaintOperation } } + /** Extract CanvasOperations if present */ + public @Nullable CanvasOperations getCanvasOperations(LayoutComponent layoutComponent) { + for (Operation op : mList) { + if (op instanceof CanvasOperations) { + ((CanvasOperations) op).setComponent(layoutComponent); + return (CanvasOperations) op; + } + } + return null; + } + /** * Extract child TextData elements * diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java index 10cbd4ca2a50..7e2a4ccec222 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java @@ -75,6 +75,7 @@ public class LayoutComponent extends Component { protected ArrayList<Component> mChildrenComponents = new ArrayList<>(); // members are not null protected boolean mChildrenHaveZIndex = false; + private CanvasOperations mDrawContentOperations; public LayoutComponent( @Nullable Component parent, @@ -138,6 +139,7 @@ public class LayoutComponent extends Component { mChildrenComponents.clear(); LayoutComponentContent content = (LayoutComponentContent) op; content.getComponents(mChildrenComponents); + mDrawContentOperations = content.getCanvasOperations(this); if (USE_IMAGE_TEMP_FIX) { if (mChildrenComponents.isEmpty() && !mContent.mList.isEmpty()) { CanvasContent canvasContent = @@ -315,6 +317,31 @@ public class LayoutComponent extends Component { } @Override + public void paint(@NonNull PaintContext context) { + if (mDrawContentOperations != null) { + context.save(); + context.translate(mX, mY); + mDrawContentOperations.paint(context); + context.restore(); + return; + } + super.paint(context); + } + + /** + * Paint the component content. Used by the DrawContent operation. (back out mX/mY -- TODO: + * refactor paintingComponent instead, to not include mX/mY etc.) + * + * @param context painting context + */ + public void drawContent(@NonNull PaintContext context) { + context.save(); + context.translate(-mX, -mY); + paintingComponent(context); + context.restore(); + } + + @Override public void paintingComponent(@NonNull PaintContext context) { Component prev = context.getContext().mLastComponent; RemoteContext remoteContext = context.getContext(); @@ -514,4 +541,11 @@ public class LayoutComponent extends Component { return null; } + + @Override + public void registerVariables(RemoteContext context) { + if (mDrawContentOperations != null) { + mDrawContentOperations.registerListening(context); + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java new file mode 100644 index 000000000000..d7abdbae4962 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java @@ -0,0 +1,125 @@ +/* + * 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.internal.widget.remotecompose.core.operations.layout.modifiers; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.layout.Component; +import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent; +import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; +import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.SerializeTags; + +import java.util.List; + +/** Represent a drawing of a component */ +public class DrawContentOperation extends Operation + implements ModifierOperation, VariableSupport, DecoratorComponent { + private static final int OP_CODE = Operations.MODIFIER_DRAW_CONTENT; + + private LayoutComponent mParent; + + public DrawContentOperation() {} + + @NonNull + @Override + public String toString() { + return "DrawContentOperation()"; + } + + /** + * Returns the serialized name for this operation + * + * @return the serialized name + */ + @NonNull + public String serializedName() { + return "DRAW_CONTENT"; + } + + @Override + public void serializeToString(int indent, @NonNull StringSerializer serializer) { + serializer.append(indent, serializedName()); + } + + @Override + public void apply(@NonNull RemoteContext context) {} + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return (indent != null ? indent : "") + toString(); + } + + @Override + public void write(@NonNull WireBuffer buffer) {} + + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + */ + public static void apply(@NonNull WireBuffer buffer) { + buffer.start(OP_CODE); + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + operations.add(new DrawContentOperation()); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Layout Operations", OP_CODE, "ComponentVisibility") + .description("This operation represents a draw of a component"); + } + + @Override + public void registerListening(@NonNull RemoteContext context) {} + + @Override + public void updateVariables(@NonNull RemoteContext context) {} + + public void setParent(@Nullable LayoutComponent parent) { + mParent = parent; + } + + @Override + public void layout( + @NonNull RemoteContext context, Component component, float width, float height) {} + + @Override + public void serialize(MapSerializer serializer) { + serializer.addTags(SerializeTags.MODIFIER).add("type", "DrawContentOperation"); + } +} diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp index 1e7bfe32ba79..66c65d0ac1aa 100644 --- a/core/jni/android_content_res_ApkAssets.cpp +++ b/core/jni/android_content_res_ApkAssets.cpp @@ -111,9 +111,8 @@ static void DeleteGuardedApkAssets(Guarded<AssetManager2::ApkAssetsPtr>& apk_ass class LoaderAssetsProvider : public AssetsProvider { public: static std::unique_ptr<AssetsProvider> Create(JNIEnv* env, jobject assets_provider) { - return (!assets_provider) ? EmptyAssetsProvider::Create() - : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider( - env, assets_provider)); + return std::unique_ptr<AssetsProvider>{ + assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr}; } bool ForEachFile(const std::string& /* root_path */, @@ -129,8 +128,8 @@ class LoaderAssetsProvider : public AssetsProvider { return debug_name_; } - bool IsUpToDate() const override { - return true; + UpToDate IsUpToDate() const override { + return UpToDate::Always; } ~LoaderAssetsProvider() override { @@ -212,7 +211,7 @@ class LoaderAssetsProvider : public AssetsProvider { auto string_result = static_cast<jstring>(env->CallObjectMethod( assets_provider_, gAssetsProviderOffsets.toString)); ScopedUtfChars str(env, string_result); - debug_name_ = std::string(str.c_str(), str.size()); + debug_name_ = std::string(str.c_str()); } // The global reference to the AssetsProvider @@ -233,9 +232,9 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma AssetManager2::ApkAssetsPtr apk_assets; switch (format) { case FORMAT_APK: { - auto assets = MultiAssetsProvider::Create(std::move(loader_assets), - ZipAssetsProvider::Create(path.c_str(), - property_flags)); + auto assets = AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(path.c_str(), + property_flags), + std::move(loader_assets)); apk_assets = ApkAssets::Load(std::move(assets), property_flags); break; } @@ -243,15 +242,17 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags); break; case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), - std::move(loader_assets), - property_flags); - break; + apk_assets = + ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), + AssetsProvider::CreateFromNullable(std::move(loader_assets)), + property_flags); + break; case FORMAT_DIRECTORY: { - auto assets = MultiAssetsProvider::Create(std::move(loader_assets), - DirectoryAssetsProvider::Create(path.c_str())); - apk_assets = ApkAssets::Load(std::move(assets), property_flags); - break; + auto assets = + AssetsProvider::CreateWithOverride(DirectoryAssetsProvider::Create(path.c_str()), + std::move(loader_assets)); + apk_assets = ApkAssets::Load(std::move(assets), property_flags); + break; } default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); @@ -308,18 +309,21 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t switch (format) { case FORMAT_APK: { auto assets = - MultiAssetsProvider::Create(std::move(loader_assets), - ZipAssetsProvider::Create(std::move(dup_fd), - friendly_name_utf8.c_str(), - property_flags)); + AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(std::move(dup_fd), + friendly_name_utf8 + .c_str(), + property_flags), + std::move(loader_assets)); apk_assets = ApkAssets::Load(std::move(assets), property_flags); break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable( - AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */), - std::move(loader_assets), property_flags); - break; + apk_assets = + ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), + nullptr /* path */), + AssetsProvider::CreateFromNullable(std::move(loader_assets)), + property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -375,23 +379,28 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ switch (format) { case FORMAT_APK: { auto assets = - MultiAssetsProvider::Create(std::move(loader_assets), - ZipAssetsProvider::Create(std::move(dup_fd), - friendly_name_utf8.c_str(), - property_flags, - static_cast<off64_t>(offset), - static_cast<off64_t>( - length))); + AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(std::move(dup_fd), + friendly_name_utf8 + .c_str(), + property_flags, + static_cast<off64_t>( + offset), + static_cast<off64_t>( + length)), + std::move(loader_assets)); apk_assets = ApkAssets::Load(std::move(assets), property_flags); break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable( - AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */, - static_cast<off64_t>(offset), - static_cast<off64_t>(length)), - std::move(loader_assets), property_flags); - break; + apk_assets = + ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), + nullptr /* path */, + static_cast<off64_t>(offset), + static_cast<off64_t>( + length)), + AssetsProvider::CreateFromNullable(std::move(loader_assets)), + property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -408,13 +417,16 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ } static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jint flags, jobject assets_provider) { - auto apk_assets = ApkAssets::Load(LoaderAssetsProvider::Create(env, assets_provider), flags); - if (apk_assets == nullptr) { - const std::string error_msg = - base::StringPrintf("Failed to load empty assets with provider %p", (void*)assets_provider); - jniThrowException(env, "java/io/IOException", error_msg.c_str()); - return 0; - } + auto apk_assets = ApkAssets::Load(AssetsProvider::CreateFromNullable( + LoaderAssetsProvider::Create(env, assets_provider)), + flags); + if (apk_assets == nullptr) { + const std::string error_msg = + base::StringPrintf("Failed to load empty assets with provider %p", + (void*)assets_provider); + jniThrowException(env, "java/io/IOException", error_msg.c_str()); + return 0; + } return CreateGuardedApkAssets(std::move(apk_assets)); } @@ -443,10 +455,10 @@ static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool()); } -static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { +static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr)); auto apk_assets = scoped_apk_assets->get(); - return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE; + return (jint)apk_assets->IsUpToDate(); } static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) { @@ -558,7 +570,7 @@ static const JNINativeMethod gApkAssetsMethods[] = { {"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName}, {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, // @CriticalNative - {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate}, + {"nativeIsUpToDate", "(J)I", (void*)NativeIsUpToDate}, {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml}, {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;", (void*)NativeGetOverlayableInfo}, diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 2ba6bc4912c3..b679688959b1 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -664,14 +664,16 @@ static void android_media_AudioSystem_vol_range_init_req_callback() static jint android_media_AudioSystem_setDeviceConnectionState(JNIEnv *env, jobject thiz, jint state, jobject jParcel, - jint codec) { + jint codec, jboolean deviceSwitch) { int status; if (Parcel *parcel = parcelForJavaObject(env, jParcel); parcel != nullptr) { android::media::audio::common::AudioPort port{}; if (status_t statusOfParcel = port.readFromParcel(parcel); statusOfParcel == OK) { - status = check_AudioSystem_Command( - AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>(state), - port, static_cast<audio_format_t>(codec))); + status = check_AudioSystem_Command( + AudioSystem::setDeviceConnectionState(static_cast<audio_policy_dev_state_t>( + state), + port, static_cast<audio_format_t>(codec), + deviceSwitch)); } else { ALOGE("Failed to read from parcel: %s", statusToString(statusOfParcel).c_str()); status = kAudioStatusError; @@ -3457,7 +3459,7 @@ static const JNINativeMethod gMethods[] = { MAKE_AUDIO_SYSTEM_METHOD(newAudioSessionId), MAKE_AUDIO_SYSTEM_METHOD(newAudioPlayerId), MAKE_AUDIO_SYSTEM_METHOD(newAudioRecorderId), - MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;I)I", + MAKE_JNI_NATIVE_METHOD("setDeviceConnectionState", "(ILandroid/os/Parcel;IZ)I", android_media_AudioSystem_setDeviceConnectionState), MAKE_AUDIO_SYSTEM_METHOD(getDeviceConnectionState), MAKE_AUDIO_SYSTEM_METHOD(handleDeviceConfigChange), diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index e0cc055a62a6..c4259f41e380 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -266,16 +266,24 @@ class NativeCommandBuffer { } // Picky version of atoi(). No sign or unexpected characters allowed. Return -1 on failure. static int digitsVal(char* start, char* end) { + constexpr int vmax = std::numeric_limits<int>::max(); int result = 0; - if (end - start > 6) { - return -1; - } for (char* dp = start; dp < end; ++dp) { if (*dp < '0' || *dp > '9') { - ALOGW("Argument failed integer format check"); + ALOGW("Argument contains non-integer characters"); + return -1; + } + int digit = *dp - '0'; + if (result > vmax / 10) { + ALOGW("Argument exceeds int limit"); + return -1; + } + result *= 10; + if (result > vmax - digit) { + ALOGW("Argument exceeds int limit"); return -1; } - result = 10 * result + (*dp - '0'); + result += digit; } return result; } diff --git a/core/res/Android.bp b/core/res/Android.bp index be4fb8bdecfb..1199d77d04c6 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -174,6 +174,7 @@ android_app { "android.media.tv.flags-aconfig", "android.security.flags-aconfig", "device_policy_aconfig_flags", + "android.xr.flags-aconfig", "com.android.hardware.input.input-aconfig", "aconfig_trade_in_mode_flags", "art-aconfig-flags", diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 51049889ecd6..78526ad4a06b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5230,6 +5230,182 @@ android:protectionLevel="signature|privileged" /> <!-- ==================================== --> + <!-- Permissions for XR perception data --> + <!-- ==================================== --> + <eat-comment /> + + <!-- Used for permissions that are associated with accessing XR + tracked information about the person using the device and the + environment around them. + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission-group android:name="android.permission-group.XR_TRACKING" + android:label="@string/permgrouplab_xr_tracking" + android:description="@string/permgroupdesc_xr_tracking" + android:priority="100" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to get approximate eye gaze. + + <p>Protection level: dangerous + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission android:name="android.permission.EYE_TRACKING_COARSE" + android:protectionLevel="dangerous" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_eye_tracking_coarse" + android:description="@string/permdesc_eye_tracking_coarse" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to get face tracking data. + + <p>Protection level: dangerous + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission android:name="android.permission.FACE_TRACKING" + android:protectionLevel="dangerous" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_face_tracking" + android:description="@string/permdesc_face_tracking" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to get hand tracking data. + + <p>Protection level: dangerous + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission android:name="android.permission.HAND_TRACKING" + android:protectionLevel="dangerous" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_hand_tracking" + android:description="@string/permdesc_hand_tracking" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to get data derived by sensing the + user's environment. + + <p>Protection level: dangerous + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission android:name="android.permission.SCENE_UNDERSTANDING_COARSE" + android:protectionLevel="dangerous" + android:permissionGroup="android.permission-group.UNDEFINED" + android:description="@string/permdesc_scene_understanding_coarse" + android:label="@string/permlab_scene_understanding_coarse" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Used for permissions that are associated with accessing + particularly sensitive XR tracking data. + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission-group android:name="android.permission-group.XR_TRACKING_SENSITIVE" + android:label="@string/permgrouplab_xr_tracking_sensitive" + android:description="@string/permgroupdesc_xr_tracking_sensitive" + android:priority="100" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to get precise eye gaze data. + + <p>Protection level: dangerous + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission android:name="android.permission.EYE_TRACKING_FINE" + android:protectionLevel="dangerous" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_eye_tracking_fine" + android:description="@string/permdesc_eye_tracking_fine" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to get head tracking data. Unmanaged + activities (OpenXR activities with the manifest property + "android.window.PROPERTY_XR_ACTIVITY_START_MODE" set to + "XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED") do not require + this permission to get head tracking data. + + {@see https://developer.android.com/develop/xr/get-started#property_activity_xr_start_mode_property} + + <p>Protection level: dangerous + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission android:name="android.permission.HEAD_TRACKING" + android:protectionLevel="dangerous" + android:permissionGroup="android.permission-group.UNDEFINED" + android:label="@string/permlab_head_tracking" + android:description="@string/permdesc_head_tracking" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to get highly precise data derived by sensing the + user's environment, such as a depth map. + + <p>Protection level: dangerous + + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) --> + <permission android:name="android.permission.SCENE_UNDERSTANDING_FINE" + android:protectionLevel="dangerous" + android:permissionGroup="android.permission-group.UNDEFINED" + android:description="@string/permdesc_scene_understanding_fine" + android:label="@string/permlab_scene_understanding_fine" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to trigger Eye Calibration, which + calibrates for IPD (inter-pupillary distance) adjustment and + eye tracking. + + <p>Protection level: signature|privileged + + @SystemApi + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + @hide --> + <permission android:name="android.permission.EYE_CALIBRATION" + android:protectionLevel="signature|privileged" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to trigger Face Tracking Calibration. + + <p>Protection level: signature|privileged + + @SystemApi + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + @hide --> + <permission android:name="android.permission.FACE_TRACKING_CALIBRATION" + android:protectionLevel="signature|privileged" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to import an anchor created and + exported by another application. + + <p>Protection level: signature|privileged + + @SystemApi + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + @hide --> + <permission android:name="android.permission.IMPORT_XR_ANCHOR" + android:protectionLevel="signature|privileged" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- Allows an application to access XR tracking data while in the + background. Without this permission, XR tracking data such as + head tracking, hand tracking, eye tracking, or face tracking + is only available to an activity it is in the + foreground. With this permission, such data is also available + to services and to activities that are in the background. + + <p>This permission must be granted in addition to the + corresponding permission such as {@link #HEAD_TRACKING} or + {@link #FACE_TRACKING} for the data being accessed. + + <p>Protection level: normal|appop + + @SystemApi + @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES) + @hide --> + <permission android:name="android.permission.XR_TRACKING_IN_BACKGROUND" + android:protectionLevel="normal|appop" + android:description="@string/permdesc_xr_tracking_in_background" + android:label="@string/permlab_xr_tracking_in_background" + android:featureFlag="android.xr.xr_manifest_entries" /> + + <!-- ==================================== --> <!-- Private permissions --> <!-- ==================================== --> <eat-comment /> @@ -7688,12 +7864,12 @@ <permission android:name="android.permission.ACCESS_SMARTSPACE" android:protectionLevel="signature|privileged|development" /> - <!-- @SystemApi Allows an application to start a contextual search. - @FlaggedApi("android.app.contextualsearch.flags.enable_service") - @hide <p>Not for use by third-party applications.</p> --> + <!-- @SystemApi Allows a system application to start a contextual search. + Other applications can start a contextual search only if they have a + foreground activity. + @hide <p>Not for use by third-party applications.</p> --> <permission android:name="android.permission.ACCESS_CONTEXTUAL_SEARCH" - android:protectionLevel="signature|privileged" - android:featureFlag="android.app.contextualsearch.flags.enable_service"/> + android:protectionLevel="signature|privileged" /> <!-- @SystemApi Allows an application to manage the wallpaper effects generation service. diff --git a/core/res/res/drawable-w192dp/loader_horizontal_watch.xml b/core/res/res/drawable-w192dp/loader_horizontal_watch.xml new file mode 100644 index 000000000000..18cea6e0d87d --- /dev/null +++ b/core/res/res/drawable-w192dp/loader_horizontal_watch.xml @@ -0,0 +1,97 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="15dp" android:width="67dp" android:viewportHeight="15" android:viewportWidth="67"> + <group android:name="_R_G"> + <group android:name="_R_G_L_1_G" android:translateX="33.5" android:translateY="7.5"> + <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M33.5 -7.5 C33.5,-7.5 33.5,7.5 33.5,7.5 C33.5,7.5 -33.5,7.5 -33.5,7.5 C-33.5,7.5 -33.5,-7.5 -33.5,-7.5 C-33.5,-7.5 33.5,-7.5 33.5,-7.5c "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="-296.5" android:translateY="-62.5" android:pivotX="330" android:pivotY="70" android:scaleX="0.1" android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-224.84700000000004" android:translateY="-321.948" android:pivotX="555.09" android:pivotY="-329" android:rotation="28.9" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M194.88 359 C190,359 185.05,357.81 180.48,355.3 C59.86,289.14 -41.55,191.9 -112.79,74.11 C-186.14,-47.16 -224.91,-186.55 -224.91,-329 C-224.91,-345.57 -211.48,-359 -194.91,-359 C-178.34,-359 -164.91,-345.57 -164.91,-329 C-164.91,-197.5 -129.13,-68.84 -61.45,43.06 C4.33,151.82 97.97,241.6 209.33,302.69 C223.86,310.66 229.18,328.9 221.21,343.42 C215.75,353.37 205.48,359 194.88,359c "/> + </group> + <group android:name="_R_G_L_0_G_L_5_G" android:translateX="744.323" android:translateY="-277.96299999999997" android:pivotX="-414.08" android:pivotY="-372.985" android:rotation="28.9"> + <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-335.95 402.99 C-351.13,402.99 -364.16,391.5 -365.76,376.07 C-367.46,359.59 -355.49,344.85 -339.01,343.14 C-162.93,324.91 -0.15,242.33 119.34,110.62 C239.66,-22.01 305.92,-193.76 305.92,-372.98 C305.92,-389.55 319.35,-402.98 335.92,-402.98 C352.49,-402.98 365.92,-389.55 365.92,-372.98 C365.92,-178.82 294.13,7.24 163.78,150.93 C34.34,293.61 -142.03,383.07 -332.83,402.82 C-333.88,402.93 -334.92,402.99 -335.95,402.99c "/> + </group> + <group android:name="_R_G_L_0_G_L_2_G" android:translateX="185.385" android:translateY="70.09100000000001" android:pivotX="144.858" android:pivotY="-721.039" android:rotation="28.9" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M144.62 58.96 C144.61,58.96 144.61,58.96 144.6,58.96 C40.39,58.93 -60.82,38.66 -156.19,-1.28 C-171.48,-7.68 -178.68,-25.26 -172.28,-40.54 C-165.88,-55.82 -148.3,-63.02 -133.02,-56.62 C-45.02,-19.77 48.4,-1.07 144.63,-1.04 C161.19,-1.03 174.62,12.4 174.62,28.97 C174.61,45.53 161.18,58.96 144.62,58.96c "/> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="330" android:translateY="70"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-660 -313 C-660,-313 -660,313 -660,313 C-660,313 660,313 660,313 C660,313 660,-313 660,-313 C660,-313 -660,-313 -660,-313c M300.74 -1.16 C205.46,38.62 103.22,59.09 -0.03,59.05 C-103.28,59.01 -205.51,38.48 -300.76,-1.37 C-316.05,-7.76 -323.26,-25.34 -316.86,-40.62 C-310.47,-55.91 -292.9,-63.12 -277.61,-56.72 C-189.68,-19.94 -95.32,-0.98 -0.01,-0.95 C95.3,-0.92 189.67,-19.81 277.63,-56.53 C292.92,-62.91 310.49,-55.69 316.87,-40.4 C323.25,-25.11 316.03,-7.54 300.74,-1.16c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.9" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.135 0.202,0.848 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> + diff --git a/core/res/res/drawable-w204dp/loader_horizontal_watch.xml b/core/res/res/drawable-w204dp/loader_horizontal_watch.xml new file mode 100644 index 000000000000..fbc6eab320eb --- /dev/null +++ b/core/res/res/drawable-w204dp/loader_horizontal_watch.xml @@ -0,0 +1,104 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="15dp" android:width="70dp" android:viewportHeight="15" android:viewportWidth="70"> + <group android:name="_R_G"> + <group android:name="_R_G_L_1_G" android:translateX="35" android:translateY="7.5"> + <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M35 -7.5 C35,-7.5 35,7.5 35,7.5 C35,7.5 -35,7.5 -35,7.5 C-35,7.5 -35,-7.5 -35,-7.5 C-35,-7.5 35,-7.5 35,-7.5c "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="-310" android:translateY="-64" android:pivotX="345" android:pivotY="71.5" android:scaleX="0.1" android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-239.44799999999998" android:translateY="-341.45" android:pivotX="584.448" android:pivotY="-346.55" android:rotation="28.8" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M205.28 376.55 C200.4,376.55 195.46,375.36 190.88,372.85 C64.08,303.29 -42.54,201.07 -117.44,77.24 C-194.55,-50.25 -235.31,-196.79 -235.31,-346.55 C-235.31,-363.12 -221.88,-376.55 -205.31,-376.55 C-188.74,-376.55 -175.31,-363.12 -175.31,-346.55 C-175.31,-207.74 -137.54,-71.93 -66.1,46.19 C3.34,160.99 102.18,255.76 219.73,320.24 C234.26,328.21 239.58,346.45 231.61,360.97 C226.15,370.92 215.88,376.55 205.28,376.55c "/> + </group> + <group android:name="_R_G_L_0_G_L_5_G" android:translateX="781.413" android:translateY="-295.124" android:pivotX="-436.413" android:pivotY="-392.876" android:rotation="28.8"> + <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-353.86 422.88 C-369.04,422.88 -382.07,411.4 -383.67,395.97 C-385.37,379.49 -373.4,364.74 -356.92,363.03 C-171.06,343.79 0.76,256.62 126.89,117.59 C253.89,-22.41 323.83,-203.7 323.83,-392.88 C323.83,-409.44 337.26,-422.88 353.83,-422.88 C370.4,-422.88 383.83,-409.44 383.83,-392.88 C383.83,-188.76 308.36,6.84 171.32,157.9 C35.25,307.89 -150.15,401.94 -350.74,422.72 C-351.79,422.82 -352.83,422.88 -353.86,422.88c "/> + </group> + <group android:name="_R_G_L_0_G_L_2_G" android:translateX="192.671" android:translateY="71.49599999999998" android:pivotX="152.329" android:pivotY="-759.496" android:rotation="28.8" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M152.33 60.5 C152.33,60.5 152.32,60.5 152.32,60.5 C42.76,60.47 -63.64,39.16 -163.91,-2.82 C-179.19,-9.22 -186.39,-26.8 -179.99,-42.08 C-173.59,-57.36 -156.02,-64.57 -140.73,-58.16 C-47.84,-19.27 50.77,0.47 152.34,0.5 C168.91,0.51 182.33,13.94 182.33,30.51 C182.32,47.08 168.89,60.5 152.33,60.5c "/> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="345" android:translateY="71.5"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-579 -259.5 C-579,-259.5 -579,259.5 -579,259.5 C-579,259.5 579,259.5 579,259.5 C579,259.5 579,-259.5 579,-259.5 C579,-259.5 -579,-259.5 -579,-259.5c M316.17 -2.8 C216,39.02 108.52,60.54 -0.03,60.5 C-108.58,60.46 -216.04,38.87 -316.18,-3.02 C-331.47,-9.41 -338.68,-26.99 -332.28,-42.27 C-325.89,-57.56 -308.32,-64.76 -293.03,-58.37 C-200.22,-19.55 -100.61,0.46 -0.01,0.5 C100.6,0.54 200.22,-19.41 293.06,-58.17 C308.35,-64.55 325.92,-57.33 332.3,-42.04 C338.68,-26.75 331.46,-9.18 316.17,-2.8c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> + diff --git a/core/res/res/drawable-w216dp/loader_horizontal_watch.xml b/core/res/res/drawable-w216dp/loader_horizontal_watch.xml new file mode 100644 index 000000000000..ed4b7ea0ff02 --- /dev/null +++ b/core/res/res/drawable-w216dp/loader_horizontal_watch.xml @@ -0,0 +1,105 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="16dp" android:width="74dp" android:viewportHeight="16" android:viewportWidth="74"> + <group android:name="_R_G"> + <group android:name="_R_G_L_1_G" android:translateX="37" android:translateY="8"> + <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M37 -8 C37,-8 37,8 37,8 C37,8 -37,8 -37,8 C-37,8 -37,-8 -37,-8 C-37,-8 37,-8 37,-8c "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="-328" android:translateY="-65.5" android:pivotX="365" android:pivotY="73.5" android:scaleX="0.1" android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-256.447" android:translateY="-365.014" android:pivotX="621.447" android:pivotY="-368.486" android:rotation="28.8" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M218.28 398.49 C213.4,398.49 208.46,397.3 203.88,394.78 C69.34,320.99 -43.78,212.53 -123.25,81.15 C-205.06,-54.11 -248.31,-209.59 -248.31,-368.49 C-248.31,-385.05 -234.88,-398.49 -218.31,-398.49 C-201.74,-398.49 -188.31,-385.05 -188.31,-368.49 C-188.31,-220.54 -148.06,-75.8 -71.91,50.09 C2.1,172.45 107.45,273.45 232.73,342.18 C247.26,350.15 252.58,368.38 244.61,382.91 C239.15,392.86 228.88,398.49 218.28,398.49c "/> + </group> + <group android:name="_R_G_L_0_G_L_5_G" android:translateX="829.0260000000001" android:translateY="-315.759" android:pivotX="-464.026" android:pivotY="-417.741" android:rotation="28.8"> + <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-376.25 447.74 C-391.43,447.74 -404.46,436.26 -406.05,420.83 C-407.76,404.35 -395.78,389.61 -379.3,387.9 C-181.22,367.38 1.9,274.48 136.32,126.3 C271.67,-22.9 346.22,-216.12 346.22,-417.74 C346.22,-434.31 359.65,-447.74 376.22,-447.74 C392.79,-447.74 406.22,-434.31 406.22,-417.74 C406.22,-201.18 326.15,6.35 180.76,166.61 C36.39,325.75 -160.31,425.54 -373.12,447.58 C-374.17,447.69 -375.22,447.74 -376.25,447.74c "/> + </group> + <group android:name="_R_G_L_0_G_L_2_G" android:translateX="203.029" android:translateY="74.06899999999996" android:pivotX="161.971" android:pivotY="-807.569" android:rotation="28.8" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M161.97 62.43 C161.97,62.43 161.96,62.43 161.96,62.43 C45.71,62.4 -67.17,39.79 -173.55,-4.75 C-188.83,-11.15 -196.03,-28.72 -189.63,-44.01 C-183.24,-59.29 -165.66,-66.49 -150.38,-60.09 C-51.37,-18.64 53.72,2.4 161.98,2.43 C178.55,2.44 191.98,15.87 191.97,32.44 C191.97,49 178.54,62.43 161.97,62.43c "/> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="365" android:translateY="73.5"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-609 -244.5 C-609,-244.5 -609,244.5 -609,244.5 C-609,244.5 609,244.5 609,244.5 C609,244.5 609,-244.5 609,-244.5 C609,-244.5 -609,-244.5 -609,-244.5c M335.44 -4.16 C229.17,40.21 115.13,63.04 -0.04,63 C-115.21,62.96 -229.22,40.05 -335.47,-4.39 C-350.76,-10.79 -357.95,-28.36 -351.56,-43.65 C-345.17,-58.93 -327.59,-66.14 -312.31,-59.74 C-213.39,-18.36 -107.24,2.96 -0.02,3 C107.21,3.04 213.38,-18.22 312.33,-59.53 C327.62,-65.91 345.19,-58.69 351.57,-43.4 C357.95,-28.11 350.73,-10.54 335.44,-4.16c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.8" android:valueTo="-51.3" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> + + diff --git a/core/res/res/drawable-w228dp/loader_horizontal_watch.xml b/core/res/res/drawable-w228dp/loader_horizontal_watch.xml new file mode 100644 index 000000000000..6b86c634d554 --- /dev/null +++ b/core/res/res/drawable-w228dp/loader_horizontal_watch.xml @@ -0,0 +1,103 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76"> + <group android:name="_R_G"> + <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8"> + <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/> + </group> + <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7"> + <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/> + </group> + <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/core/res/res/drawable-w240dp/loader_horizontal_watch.xml b/core/res/res/drawable-w240dp/loader_horizontal_watch.xml new file mode 100644 index 000000000000..ad60bbdc420c --- /dev/null +++ b/core/res/res/drawable-w240dp/loader_horizontal_watch.xml @@ -0,0 +1,104 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="17dp" android:width="82dp" android:viewportHeight="17" android:viewportWidth="82"> + <group android:name="_R_G"> + <group android:name="_R_G_L_1_G" android:translateX="41" android:translateY="8.5"> + <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M41 -8.5 C41,-8.5 41,8.5 41,8.5 C41,8.5 -41,8.5 -41,8.5 C-41,8.5 -41,-8.5 -41,-8.5 C-41,-8.5 41,-8.5 41,-8.5c "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="-362.5" android:translateY="-69" android:pivotX="403.5" android:pivotY="77.5" android:scaleX="0.1" android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-291.64799999999997" android:translateY="-414.141" android:pivotX="695.448" android:pivotY="-412.359" android:rotation="28.7" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M244.28 442.36 C239.4,442.36 234.45,441.17 229.88,438.66 C79.87,356.38 -46.26,235.46 -134.87,88.96 C-179.66,14.91 -214.28,-64.74 -237.78,-147.79 C-262.02,-233.47 -274.31,-322.49 -274.31,-412.36 C-274.31,-428.93 -260.88,-442.36 -244.31,-442.36 C-227.74,-442.36 -214.31,-428.93 -214.31,-412.36 C-214.31,-328.01 -202.78,-244.49 -180.05,-164.12 C-158.01,-86.24 -125.54,-11.54 -83.53,57.91 C-0.38,195.38 117.97,308.85 258.73,386.05 C273.26,394.02 278.58,412.26 270.61,426.78 C265.15,436.73 254.88,442.36 244.28,442.36c "/> + </group> + <group android:name="_R_G_L_0_G_L_5_G" android:translateX="923.0530000000001" android:translateY="-359.029" android:pivotX="-519.253" android:pivotY="-467.471" android:rotation="28.7"> + <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-421.02 497.47 C-436.2,497.47 -449.23,485.99 -450.83,470.56 C-452.53,454.08 -440.56,439.34 -424.08,437.63 C-201.54,414.57 4.18,310.19 155.19,143.73 C229.54,61.77 287.7,-31.75 328.04,-134.22 C369.81,-240.3 390.99,-352.42 390.99,-467.47 C390.99,-484.04 404.42,-497.47 420.99,-497.47 C437.56,-497.47 450.99,-484.04 450.99,-467.47 C450.99,-226.02 361.72,5.35 199.63,184.04 C38.67,361.47 -180.63,472.73 -417.89,497.31 C-418.94,497.42 -419.99,497.47 -421.02,497.47c "/> + </group> + <group android:name="_R_G_L_0_G_L_2_G" android:translateX="222.54600000000002" android:translateY="77.21400000000006" android:pivotX="181.254" android:pivotY="-903.714" android:rotation="28.7" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M181.26 66.28 C181.25,66.28 181.25,66.28 181.25,66.28 C51.64,66.25 -74.22,41.06 -192.83,-8.6 C-208.12,-15 -215.32,-32.58 -208.92,-47.86 C-202.52,-63.15 -184.94,-70.35 -169.66,-63.95 C-58.42,-17.38 59.64,6.25 181.26,6.28 C197.83,6.29 211.26,19.72 211.26,36.29 C211.25,52.86 197.82,66.28 181.26,66.28c "/> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="403.5" android:translateY="77.5"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-630.5 -255.5 C-630.5,-255.5 -630.5,255.5 -630.5,255.5 C-630.5,255.5 630.5,255.5 630.5,255.5 C630.5,255.5 630.5,-255.5 630.5,-255.5 C630.5,-255.5 -630.5,-255.5 -630.5,-255.5c M374 -8.88 C255.5,40.59 128.4,66.04 0,66 C-128.4,65.95 -255.6,40.42 -374,-9.14 C-389.3,-15.53 -396.5,-33.11 -390.1,-48.39 C-383.7,-63.68 -366.2,-70.88 -350.9,-64.49 C-239.7,-18 -120.5,5.96 0,6 C120.4,6.04 239.7,-17.84 350.9,-64.25 C366.2,-70.63 383.7,-63.41 390.1,-48.12 C396.5,-32.83 389.3,-15.26 374,-8.88c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> + diff --git a/core/res/res/drawable/ic_notification_summarization.xml b/core/res/res/drawable/ic_notification_summarization.xml index de905fa10728..d476872a0e20 100644 --- a/core/res/res/drawable/ic_notification_summarization.xml +++ b/core/res/res/drawable/ic_notification_summarization.xml @@ -19,5 +19,6 @@ Copyright (C) 2025 The Android Open Source Project android:tint="?android:attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960"> - <path android:fillColor="#ffffff" android:pathData="M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/> + <path android:fillColor="#ffffff" + android:pathData="M120,840L120,760L600,760L600,840L120,840ZM120,640L120,560L840,560L840,640L120,640ZM120,440L120,360L560,360L560,440L120,440ZM700,480Q700,388 636,324Q572,260 480,260Q572,260 636,196Q700,132 700,40Q700,132 764,196Q828,260 920,260Q828,260 764,324Q700,388 700,480Z"/> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/loader_horizontal_watch.xml b/core/res/res/drawable/loader_horizontal_watch.xml new file mode 100644 index 000000000000..6b86c634d554 --- /dev/null +++ b/core/res/res/drawable/loader_horizontal_watch.xml @@ -0,0 +1,103 @@ +<!-- + ~ Copyright (C) 2025 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. + --> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="14dp" android:width="76dp" android:viewportHeight="14" android:viewportWidth="76"> + <group android:name="_R_G"> + <group android:name="_R_G_L_1_G" android:translateX="39" android:translateY="8"> + <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M39 -8 C39,-8 39,8 39,8 C39,8 -39,8 -39,8 C-39,8 -39,-8 -39,-8 C-39,-8 39,-8 39,-8c "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="-345" android:translateY="-67" android:pivotX="384" android:pivotY="75" android:scaleX="0.1" android:scaleY="0.1"> + <group android:name="_R_G_L_0_G_L_6_G" android:translateX="-274.19" android:translateY="-390.077" android:pivotX="658.448" android:pivotY="-390.423" android:rotation="28.7" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_6_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M231.28 420.42 C226.4,420.42 221.45,419.23 216.88,416.72 C74.61,338.68 -45.02,224 -129.06,85.06 C-171.54,14.82 -204.38,-60.73 -226.66,-139.5 C-249.65,-220.76 -261.31,-305.18 -261.31,-390.42 C-261.31,-406.99 -247.88,-420.42 -231.31,-420.42 C-214.74,-420.42 -201.31,-406.99 -201.31,-390.42 C-201.31,-310.71 -190.42,-231.78 -168.93,-155.83 C-148.11,-82.23 -117.42,-11.63 -77.72,54 C0.86,183.92 112.71,291.15 245.73,364.12 C260.26,372.08 265.58,390.32 257.61,404.85 C252.15,414.79 241.88,420.42 231.28,420.42c "/> + </group> + <group android:name="_R_G_L_0_G_L_5_G" android:translateX="875.8979999999999" android:translateY="-337.894" android:pivotX="-491.64" android:pivotY="-442.606" android:rotation="28.7"> + <path android:name="_R_G_L_0_G_L_5_G_D_0_P_0" android:fillColor="#303030" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-398.64 472.61 C-413.82,472.61 -426.84,461.13 -428.44,445.7 C-430.15,429.22 -418.17,414.47 -401.69,412.77 C-191.38,390.98 3.04,292.33 145.75,135.01 C289.46,-23.4 368.6,-228.54 368.6,-442.61 C368.6,-459.17 382.04,-472.61 398.6,-472.61 C415.17,-472.61 428.6,-459.17 428.6,-442.61 C428.6,-213.6 343.93,5.85 190.19,175.33 C37.53,343.61 -170.48,449.13 -395.51,472.44 C-396.56,472.55 -397.6,472.61 -398.64,472.61c "/> + </group> + <group android:name="_R_G_L_0_G_L_2_G" android:translateX="212.64499999999998" android:translateY="75.14200000000005" android:pivotX="171.613" android:pivotY="-855.642" android:rotation="28.7" android:scaleY="0"> + <path android:name="_R_G_L_0_G_L_2_G_D_0_P_0" android:fillColor="#ffffff" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M171.61 64.36 C171.61,64.36 171.61,64.36 171.61,64.36 C48.68,64.32 -70.7,40.42 -183.19,-6.68 C-198.47,-13.07 -205.68,-30.65 -199.28,-45.93 C-192.88,-61.22 -175.3,-68.42 -160.02,-62.02 C-54.9,-18.01 56.68,4.33 171.62,4.36 C188.19,4.36 201.62,17.8 201.61,34.36 C201.61,50.93 188.18,64.36 171.61,64.36c "/> + </group> + <group android:name="_R_G_L_0_G_L_0_G" android:translateX="384" android:translateY="75"> + <path android:name="_R_G_L_0_G_L_0_G_D_0_P_0" android:fillColor="#000000" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-611 -259 C-611,-259 -611,259 -611,259 C-611,259 611,259 611,259 C611,259 611,-259 611,-259 C611,-259 -611,-259 -611,-259c M354.66 -6.52 C242.36,40.4 121.76,64.54 -0.04,64.5 C-121.84,64.46 -242.44,40.23 -354.74,-6.76 C-370.04,-13.16 -377.24,-30.73 -370.84,-46.02 C-364.44,-61.3 -346.94,-68.51 -331.64,-62.12 C-226.54,-18.18 -113.84,4.46 -0.04,4.5 C113.76,4.54 226.56,-18.02 331.56,-61.89 C346.86,-68.27 364.46,-61.05 370.86,-45.76 C377.26,-30.47 369.96,-12.9 354.66,-6.52c "/> + </group> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_6_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="333" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_5_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="rotation" android:duration="983" android:startOffset="0" android:valueFrom="28.7" android:valueTo="-51.4" android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.402,0.136 0.202,0.847 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_L_2_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="scaleY" android:duration="0" android:startOffset="133" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:repeatCount="infinite" android:propertyName="translateX" android:duration="1000" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index 75bd244cbbf4..1bde17358825 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -29,7 +29,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" + android:textSize="16sp" android:singleLine="true" android:layout_weight="1" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index 054583297d37..d29b7af9e24e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -102,7 +102,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 9959b666b3bf..5beab508aecf 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -104,7 +104,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index 85ca124de8ff..d7c3263904d4 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -130,7 +130,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml index 11fc48668ad7..52bc7b8ea3bb 100644 --- a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml +++ b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml @@ -69,7 +69,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml index bf70a5eff47e..cf9ff6bef6f8 100644 --- a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml +++ b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml @@ -90,7 +90,6 @@ android:singleLine="true" android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@dimen/notification_2025_title_text_size" /> <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/values-w192dp/dimens_watch.xml b/core/res/res/values-w192dp/dimens_watch.xml new file mode 100644 index 000000000000..c6bf767ab6b8 --- /dev/null +++ b/core/res/res/values-w192dp/dimens_watch.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <!-- 16.7% of display size --> + <dimen name="base_error_dialog_top_padding">32dp</dimen> + <!-- 5.2% of display size --> + <dimen name="base_error_dialog_padding">10dp</dimen> + <!-- 20.83% of display size --> + <dimen name="base_error_dialog_bottom_padding">40dp</dimen> + + <!-- watch's indeterminate progress bar dimens based on the current screen size --> + <dimen name="loader_horizontal_min_width_watch">67dp</dimen> + <dimen name="loader_horizontal_min_height_watch">15dp</dimen> +</resources> diff --git a/core/res/res/values-w204dp-round-watch/dimens_watch.xml b/core/res/res/values-w204dp-round-watch/dimens_watch.xml new file mode 100644 index 000000000000..3509474c5c2e --- /dev/null +++ b/core/res/res/values-w204dp-round-watch/dimens_watch.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <!-- watch's indeterminate progress bar dimens based on the current screen size --> + <dimen name="loader_horizontal_min_width_watch">70dp</dimen> + <dimen name="loader_horizontal_min_height_watch">15dp</dimen> +</resources> diff --git a/core/res/res/values-w216dp/dimens_watch.xml b/core/res/res/values-w216dp/dimens_watch.xml new file mode 100644 index 000000000000..e14ce5e98f55 --- /dev/null +++ b/core/res/res/values-w216dp/dimens_watch.xml @@ -0,0 +1,21 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <!-- watch's indeterminate progress bar dimens based on the current screen size --> + <dimen name="loader_horizontal_min_width_watch">72dp</dimen> + <dimen name="loader_horizontal_min_height_watch">16dp</dimen> +</resources>
\ No newline at end of file diff --git a/core/res/res/values-w228dp/dimens_watch.xml b/core/res/res/values-w228dp/dimens_watch.xml new file mode 100644 index 000000000000..3c6265690c3c --- /dev/null +++ b/core/res/res/values-w228dp/dimens_watch.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <!-- watch's indeterminate progress bar dimens based on the current screen size --> + <dimen name="loader_horizontal_min_width">76dp</dimen> + <dimen name="loader_horizontal_min_height">14dp</dimen> +</resources> diff --git a/core/res/res/values-w240dp/dimens_material.xml b/core/res/res/values-w240dp/dimens_material.xml index bd26c8bf84df..e30aea46aec7 100644 --- a/core/res/res/values-w240dp/dimens_material.xml +++ b/core/res/res/values-w240dp/dimens_material.xml @@ -21,4 +21,8 @@ <dimen name="screen_percentage_12">28.8dp</dimen> <dimen name="screen_percentage_15">36dp</dimen> <dimen name="screen_percentage_3646">87.5dp</dimen> + + <!-- watch's indeterminate progress bar dimens based on the current screen size --> + <dimen name="progress_indeterminate_horizontal_min_width_watch">80dp</dimen> + <dimen name="progress_indeterminate_horizontal_min_height_watch">17dp</dimen> </resources> diff --git a/core/res/res/values-watch/styles_device_defaults.xml b/core/res/res/values-watch/styles_device_defaults.xml index fb7dbb0660c5..eeb66e7cf6a8 100644 --- a/core/res/res/values-watch/styles_device_defaults.xml +++ b/core/res/res/values-watch/styles_device_defaults.xml @@ -42,5 +42,8 @@ <item name="indeterminateOnly">false</item> <!-- Use Wear Material3 ring shape as default determinate drawable --> <item name="progressDrawable">@drawable/progress_ring_watch</item> + <item name="indeterminateDrawable">@drawable/loader_horizontal_watch</item> + <item name="android:minWidth">@dimen/loader_horizontal_min_width_watch</item> + <item name="android:minHeight">@dimen/loader_horizontal_min_height_watch</item> </style> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1a311d572e0b..2188469bdf03 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2643,6 +2643,15 @@ <!-- MMS user agent prolfile url --> <string name="config_mms_user_agent_profile_url" translatable="false"></string> + <!-- The default list of possible CMF Names|Style|ColorSource. This array can be + overridden device-specific resources. A wildcard (fallback) must be supplied. + Name - Read from `ro.boot.hardware.color` sysprop. Fallback (*) required. + Styles - frameworks/libs/systemui/monet/src/com/android/systemui/monet/Style.java + Color - `home_wallpaper` (for color extraction) or a hexadecimal int (#FFcc99) --> + <string-array name="theming_defaults"> + <item>*|TONAL_SPOT|home_wallpaper</item> + </string-array> + <!-- National Language Identifier codes for the following two config items. (from 3GPP TS 23.038 V9.1.1 Table 6.2.1.2.4.1): 0 - reserved diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index e9d87e4b5f5b..9acb2427aaab 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -580,9 +580,6 @@ <dimen name="notification_text_size">14sp</dimen> <!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) --> <dimen name="notification_title_text_size">14sp</dimen> - <!-- Size of notification text titles, 2025 redesign version (see TextAppearance.StatusBar.EventContent.Title) --> - <!-- TODO: b/378660052 - When inlining the redesign flag, this should be updated directly in TextAppearance.DeviceDefault.Notification.Title --> - <dimen name="notification_2025_title_text_size">16sp</dimen> <!-- Size of big notification text titles (see TextAppearance.StatusBar.EventContent.BigTitle) --> <dimen name="notification_big_title_text_size">16sp</dimen> <!-- Size of smaller notification text (see TextAppearance.StatusBar.EventContent.Line2, Info, Time) --> diff --git a/core/res/res/values/dimens_watch.xml b/core/res/res/values/dimens_watch.xml index 2aae98715973..19845916984b 100644 --- a/core/res/res/values/dimens_watch.xml +++ b/core/res/res/values/dimens_watch.xml @@ -61,4 +61,8 @@ <dimen name="disabled_alpha_wear_material3">0.12</dimen> <!-- Alpha transparency applied to elements which are considered primary (e.g. primary text) --> <dimen name="primary_content_alpha_wear_material3">0.38</dimen> + + <!-- watch's indeterminate progress bar dimens --> + <dimen name="loader_horizontal_min_width">68dp</dimen> + <dimen name="loader_horizontal_min_height">13dp</dimen> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index abbba9d1bffa..7a93ca1e9ac6 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1011,6 +1011,16 @@ <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]--> <string name="permgroupdesc_notifications">show notifications</string> + <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]--> + <string name="permgrouplab_xr_tracking">XR tracking data</string> + <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]--> + <string name="permgroupdesc_xr_tracking">access XR data about you and the environment around you</string> + + <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]--> + <string name="permgrouplab_xr_tracking_sensitive">sensitive XR tracking data</string> + <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=NONE]--> + <string name="permgroupdesc_xr_tracking_sensitive">access sensitive tracking data, such as eye gaze</string> + <!-- Title for the capability of an accessibility service to retrieve window content. --> <string name="capability_title_canRetrieveWindowContent">Retrieve window content</string> <!-- Description for the capability of an accessibility service to retrieve window content. --> @@ -1875,6 +1885,45 @@ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_mediaLocation">Allows the app to read locations from your media collection.</string> + <string name="permlab_eye_tracking_coarse">track your approximate eye gaze</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_eye_tracking_coarse">Allows the app to track your approximate eye gaze.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_eye_tracking_fine">track where you are looking</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_eye_tracking_fine">Allows the app to access precise eye gaze data.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_face_tracking">track your face</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_face_tracking">Allows the app to access face tracking data.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_hand_tracking">track your hands</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_hand_tracking">Allows the app to access hand tracking data.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_head_tracking">track your head</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_head_tracking">Allows the app to access head tracking data.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_scene_understanding_coarse">understand your immediate environment</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_scene_understanding_coarse">Allows the app to access tracking data about the environment directly around you.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_scene_understanding_fine">understand your immediate environment at high detail</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_scene_understanding_fine">Allows the app to access tracking data about the environment directly around you with very high detail.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_xr_tracking_in_background">access XR data while not in the foreground</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_xr_tracking_in_background">Allows the app to access XR data while not in the foreground.</string> + <!-- Name for an app setting that lets the user authenticate for that app using biometrics (e.g. fingerprint or face). [CHAR LIMIT=30] --> <string name="biometric_app_setting_name">Use biometrics</string> <!-- Name for an app setting that lets the user authenticate for that app using biometrics (e.g. fingerprint or face) or their screen lock credential (i.e. PIN, pattern, or password). [CHAR LIMIT=70] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c62732d36038..ffcfce9c420e 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -575,7 +575,6 @@ <java-symbol type="dimen" name="notification_text_size" /> <java-symbol type="dimen" name="notification_title_text_size" /> <java-symbol type="dimen" name="notification_subtext_size" /> - <java-symbol type="dimen" name="notification_2025_title_text_size" /> <java-symbol type="dimen" name="notification_top_pad" /> <java-symbol type="dimen" name="notification_top_pad_narrow" /> <java-symbol type="dimen" name="notification_top_pad_large_text" /> @@ -5904,6 +5903,9 @@ <java-symbol type="drawable" name="ic_notification_summarization" /> <java-symbol type="dimen" name="notification_collapsed_height_with_summarization" /> + <!-- Device CMF Theming Settings --> + <java-symbol type="array" name="theming_defaults" /> + <!-- Advanced Protection Service USB feature --> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_title" /> <java-symbol type="string" name="usb_apm_usb_plugged_in_when_locked_notification_text" /> diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java index 5765562e2383..6fe3b6ca0c6c 100644 --- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java +++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java @@ -26,7 +26,6 @@ import static org.junit.Assert.assertThrows; import android.content.ComponentName; import android.net.Uri; import android.os.Parcel; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; @@ -112,7 +111,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void testLongInputsFromParcel() { // Create a rule with long fields, set directly via reflection so that we can confirm that // a rule with too-long fields that comes in via a parcel has its fields truncated directly. @@ -169,7 +167,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void builderConstructor_nullInputs_throws() { assertThrows(NullPointerException.class, () -> new AutomaticZenRule.Builder(null, Uri.parse("condition"))); @@ -178,7 +175,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void constructor_defaultTypeUnknown() { AutomaticZenRule rule = new AutomaticZenRule("name", new ComponentName("pkg", "cps"), null, Uri.parse("conditionId"), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, @@ -188,7 +184,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void builder_defaultsAreSensible() { AutomaticZenRule rule = new AutomaticZenRule.Builder("name", Uri.parse("conditionId")).build(); @@ -200,7 +195,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void validate_builderWithValidType_succeeds() throws Exception { AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")) .setType(AutomaticZenRule.TYPE_BEDTIME) @@ -209,14 +203,12 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void validate_builderWithoutType_succeeds() throws Exception { AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build(); rule.validate(); // No exception. } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void validate_constructorWithoutType_succeeds() throws Exception { AutomaticZenRule rule = new AutomaticZenRule("rule", new ComponentName("pkg", "cps"), new ComponentName("pkg", "activity"), Uri.parse("condition"), null, @@ -225,7 +217,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void validate_invalidType_throws() throws Exception { AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build(); @@ -238,7 +229,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void setType_invalidType_throws() { AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", Uri.parse("uri")).build(); @@ -246,7 +236,6 @@ public class AutomaticZenRuleTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void setTypeBuilder_invalidType_throws() { AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("rule", Uri.parse("uri")); diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index d816039d0d3c..250b9ce8d89d 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -521,7 +521,7 @@ public class NotificationManagerTest { } @Test - @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @EnableFlags(Flags.FLAG_MODES_UI) public void areAutomaticZenRulesUserManaged_handheld_isTrue() { PackageManager pm = mock(PackageManager.class); when(pm.hasSystemFeature(any())).thenReturn(false); @@ -531,7 +531,7 @@ public class NotificationManagerTest { } @Test - @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @EnableFlags(Flags.FLAG_MODES_UI) public void areAutomaticZenRulesUserManaged_auto_isFalse() { PackageManager pm = mock(PackageManager.class); when(pm.hasSystemFeature(eq(PackageManager.FEATURE_AUTOMOTIVE))).thenReturn(true); @@ -541,7 +541,7 @@ public class NotificationManagerTest { } @Test - @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @EnableFlags(Flags.FLAG_MODES_UI) public void areAutomaticZenRulesUserManaged_tv_isFalse() { PackageManager pm = mock(PackageManager.class); when(pm.hasSystemFeature(eq(PackageManager.FEATURE_LEANBACK))).thenReturn(true); @@ -551,7 +551,7 @@ public class NotificationManagerTest { } @Test - @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @EnableFlags(Flags.FLAG_MODES_UI) public void areAutomaticZenRulesUserManaged_watch_isFalse() { PackageManager pm = mock(PackageManager.class); when(pm.hasSystemFeature(eq(PackageManager.FEATURE_WATCH))).thenReturn(true); diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index dc2f0a69375d..9383807ec761 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -33,6 +33,7 @@ import android.content.Context; import android.os.Handler; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -348,6 +349,26 @@ public class DisplayManagerGlobalTest { DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_BRIGHTNESS)); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT) + public void test_mapPrivateEventCommittedStateChanged_flagEnabled() { + // Test public flags mapping + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED, + mDisplayManagerGlobal + .mapFiltersToInternalEventFlag(0, + DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED)); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT) + public void test_mapPrivateEventCommittedStateChanged_flagDisabled() { + // Test public flags mapping + assertEquals(0, + mDisplayManagerGlobal + .mapFiltersToInternalEventFlag(0, + DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_COMMITTED_STATE_CHANGED)); + } + private void waitForHandler() { mHandler.runWithScissors(() -> { }, 0); diff --git a/core/tests/coretests/src/android/service/notification/ConditionTest.java b/core/tests/coretests/src/android/service/notification/ConditionTest.java index e94273e1ada7..65c108a827ef 100644 --- a/core/tests/coretests/src/android/service/notification/ConditionTest.java +++ b/core/tests/coretests/src/android/service/notification/ConditionTest.java @@ -23,10 +23,8 @@ import static junit.framework.Assert.fail; import static org.junit.Assert.assertThrows; -import android.app.Flags; import android.net.Uri; import android.os.Parcel; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -113,7 +111,6 @@ public class ConditionTest { @Test public void testLongFields_inConstructors() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); String longString = Strings.repeat("A", 65536); Uri longUri = Uri.parse("uri://" + Strings.repeat("A", 65530)); @@ -136,7 +133,6 @@ public class ConditionTest { @Test public void testLongFields_viaParcel() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); // Set fields via reflection to force them to be long, then parcel and unparcel to make sure // it gets truncated upon unparcelling. Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder", @@ -170,8 +166,6 @@ public class ConditionTest { @Test public void testEquals() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - Condition cond1 = new Condition(Uri.parse("uri://placeholder"), "placeholder", Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION); Condition cond2 = new Condition(Uri.parse("uri://placeholder"), "placeholder", @@ -186,8 +180,6 @@ public class ConditionTest { @Test public void testParcelConstructor() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - Condition cond = new Condition(Uri.parse("uri://placeholder"), "placeholder", Condition.STATE_TRUE, Condition.SOURCE_USER_ACTION); @@ -200,28 +192,24 @@ public class ConditionTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void constructor_unspecifiedSource_succeeds() { new Condition(Uri.parse("id"), "Summary", Condition.STATE_TRUE); // No exception. } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void constructor_validSource_succeeds() { new Condition(Uri.parse("id"), "Summary", Condition.STATE_TRUE, Condition.SOURCE_CONTEXT); // No exception. } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void constructor_invalidSource_throws() { assertThrows(IllegalArgumentException.class, () -> new Condition(Uri.parse("uri"), "Summary", Condition.STATE_TRUE, 1000)); } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void constructor_parcelWithInvalidSource_throws() { Condition original = new Condition(Uri.parse("condition"), "Summary", Condition.STATE_TRUE, Condition.SOURCE_SCHEDULE); @@ -237,7 +225,6 @@ public class ConditionTest { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void validate_invalidSource_throws() throws Exception { Condition condition = new Condition(Uri.parse("condition"), "Summary", Condition.STATE_TRUE, Condition.SOURCE_SCHEDULE); diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 9f398ecf5492..b7d6ab56d6b3 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -79,6 +79,7 @@ import android.graphics.drawable.Icon; import android.metrics.LogMaker; import android.net.Uri; import android.os.UserHandle; +import android.os.UserManager; import android.provider.DeviceConfig; import android.service.chooser.ChooserTarget; import android.util.Pair; @@ -3178,7 +3179,11 @@ public class ChooserActivityTest { } private void markWorkProfileUserAvailable() { - ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); + if (UserManager.isHeadlessSystemUserMode()) { + ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(11); + } else { + ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); + } } private void markCloneProfileUserAvailable() { diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index dcea9120e2a5..be7f84e3ea8f 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -155,12 +155,12 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW @Override protected ResolverListController createListController(UserHandle userHandle) { - if (userHandle == UserHandle.SYSTEM) { - when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); - return sOverrides.resolverListController; + if (userHandle.equals(sOverrides.workProfileUserHandle)) { + when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.workResolverListController; } - when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); - return sOverrides.workResolverListController; + when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.resolverListController; } @Override diff --git a/graphics/java/android/graphics/FrameInfo.java b/graphics/java/android/graphics/FrameInfo.java index 7d236d203201..3b8f46630344 100644 --- a/graphics/java/android/graphics/FrameInfo.java +++ b/graphics/java/android/graphics/FrameInfo.java @@ -93,10 +93,12 @@ public final class FrameInfo { // Interval between two consecutive frames public static final int FRAME_INTERVAL = 11; + // Workload target deadline for a frame + public static final int WORKLOAD_TARGET = 12; + // Must be the last one // This value must be in sync with `UI_THREAD_FRAME_INFO_SIZE` in FrameInfo.h - // In calculating size, + 1 for Flags, and + 1 for WorkloadTarget from FrameInfo.h - private static final int FRAME_INFO_SIZE = FRAME_INTERVAL + 2; + private static final int FRAME_INFO_SIZE = WORKLOAD_TARGET + 1; /** checkstyle */ public void setVsync(long intendedVsync, long usedVsync, long frameTimelineVsyncId, @@ -108,6 +110,7 @@ public final class FrameInfo { frameInfo[FRAME_DEADLINE] = frameDeadline; frameInfo[FRAME_START_TIME] = frameStartTime; frameInfo[FRAME_INTERVAL] = frameInterval; + frameInfo[WORKLOAD_TARGET] = frameDeadline - intendedVsync; } /** checkstyle */ diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index 940cd93c53f2..65854dd51a91 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -1467,6 +1467,18 @@ public class HardwareRenderer { public static native void preload(); /** + * Initialize the Buffer Allocator singleton + * + * This takes 10-20ms on low-resourced devices, so doing it on-demand when an app + * tries to render its first frame causes drawFrames to be blocked for buffer + * allocation due to just initializing the allocator. + * + * Should only be called when a buffer is expected to be used. + * @hide + */ + public static native void preInitBufferAllocator(); + + /** * @hide */ protected static native boolean isWebViewOverlaysEnabled(); diff --git a/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java index 3543e991924e..db2376e008f5 100644 --- a/graphics/java/android/graphics/RuntimeShader.java +++ b/graphics/java/android/graphics/RuntimeShader.java @@ -20,6 +20,7 @@ import android.annotation.ColorInt; import android.annotation.ColorLong; import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.ArrayMap; import android.view.Window; @@ -76,6 +77,7 @@ import libcore.util.NativeAllocationRegistry; * Additionally, if the shader is invoked by another using {@link #setInputShader(String, Shader)}, * then that parent shader may modify the input coordinates arbitrarily.</p> * + * <a id="agsl-and-color-spaces"/> * <h3>AGSL and Color Spaces</h3> * <p>Android Graphics and by extension {@link RuntimeShader} are color managed. The working * {@link ColorSpace} for an AGSL shader is defined to be the color space of the destination, which @@ -267,6 +269,8 @@ public class RuntimeShader extends Shader { private ArrayMap<String, ColorFilter> mColorFilterUniforms = new ArrayMap<>(); private ArrayMap<String, RuntimeXfermode> mXfermodeUniforms = new ArrayMap<>(); + private ColorSpace mWorkingColorSpace = null; + /** * Creates a new RuntimeShader. @@ -286,6 +290,35 @@ public class RuntimeShader extends Shader { } /** + * Sets the working color space for this shader. That is, the shader will be evaluated + * in the given colorspace before being converted to the output destination's colorspace. + * + * <p>By default the RuntimeShader is evaluated in the context of the + * <a href="#agsl-and-color-spaces">destination colorspace</a>. By calling this method + * that can be overridden to force the shader to be evaluated in the given colorspace first + * before then being color converted to the destination colorspace.</p> + * + * @param colorSpace The ColorSpace to evaluate in. Must be an {@link ColorSpace#getModel() RGB} + * ColorSpace. Passing null restores default behavior of working in the + * destination colorspace. + * @throws IllegalArgumentException If the colorspace is not RGB + */ + @FlaggedApi(Flags.FLAG_SHADER_COLOR_SPACE) + public void setWorkingColorSpace(@Nullable ColorSpace colorSpace) { + if (colorSpace != null && colorSpace.getModel() != ColorSpace.Model.RGB) { + throw new IllegalArgumentException("ColorSpace must be RGB, given " + colorSpace); + } + if (mWorkingColorSpace != colorSpace) { + mWorkingColorSpace = colorSpace; + if (mWorkingColorSpace != null) { + // Just to enforce this can be resolved instead of erroring out later + mWorkingColorSpace.getNativeInstance(); + } + discardNativeInstance(); + } + } + + /** * Sets the uniform color value corresponding to this shader. If the shader does not have a * uniform with that name or if the uniform is declared with a type other than vec3 or vec4 and * corresponding layout(color) annotation then an IllegalArgumentException is thrown. @@ -578,7 +611,8 @@ public class RuntimeShader extends Shader { /** @hide */ @Override protected long createNativeInstance(long nativeMatrix, boolean filterFromPaint) { - return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix); + return nativeCreateShader(mNativeInstanceRuntimeShaderBuilder, nativeMatrix, + mWorkingColorSpace != null ? mWorkingColorSpace.getNativeInstance() : 0); } /** @hide */ @@ -589,6 +623,8 @@ public class RuntimeShader extends Shader { private static native long nativeGetFinalizer(); private static native long nativeCreateBuilder(String agsl); private static native long nativeCreateShader(long shaderBuilder, long matrix); + private static native long nativeCreateShader(long shaderBuilder, long matrix, + long colorSpacePtr); private static native void nativeUpdateUniforms( long shaderBuilder, String uniformName, float[] uniforms, boolean isColor); private static native void nativeUpdateUniforms( diff --git a/keystore/java/android/security/GateKeeper.java b/keystore/java/android/security/GateKeeper.java index 464714fe2895..c2792e1f2394 100644 --- a/keystore/java/android/security/GateKeeper.java +++ b/keystore/java/android/security/GateKeeper.java @@ -28,7 +28,7 @@ import android.service.gatekeeper.IGateKeeperService; * * @hide */ -public abstract class GateKeeper { +public final class GateKeeper { public static final long INVALID_SECURE_USER_ID = 0; diff --git a/keystore/java/android/security/keystore/ArrayUtils.java b/keystore/java/android/security/keystore/ArrayUtils.java index f22b6041800f..6472ca9957d0 100644 --- a/keystore/java/android/security/keystore/ArrayUtils.java +++ b/keystore/java/android/security/keystore/ArrayUtils.java @@ -23,7 +23,7 @@ import java.util.function.Consumer; /** * @hide */ -public abstract class ArrayUtils { +public final class ArrayUtils { private ArrayUtils() {} public static String[] nullToEmpty(String[] array) { diff --git a/keystore/java/android/security/keystore/Utils.java b/keystore/java/android/security/keystore/Utils.java index e58b1ccb5370..c38ce8e86a15 100644 --- a/keystore/java/android/security/keystore/Utils.java +++ b/keystore/java/android/security/keystore/Utils.java @@ -23,7 +23,7 @@ import java.util.Date; * * @hide */ -abstract class Utils { +public final class Utils { private Utils() {} static Date cloneIfNotNull(Date value) { diff --git a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java index 1394bd443f03..9d306ce1ed38 100644 --- a/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java +++ b/keystore/java/android/security/keystore2/KeyStore2ParameterUtils.java @@ -38,7 +38,9 @@ import java.util.function.Consumer; /** * @hide */ -public abstract class KeyStore2ParameterUtils { +public final class KeyStore2ParameterUtils { + + private KeyStore2ParameterUtils() {} /** * This function constructs a {@link KeyParameter} expressing a boolean value. diff --git a/keystore/java/android/security/keystore2/KeymasterUtils.java b/keystore/java/android/security/keystore2/KeymasterUtils.java index 614e3684c417..02f3f578d03e 100644 --- a/keystore/java/android/security/keystore2/KeymasterUtils.java +++ b/keystore/java/android/security/keystore2/KeymasterUtils.java @@ -16,13 +16,10 @@ package android.security.keystore2; -import android.security.keymaster.KeymasterArguments; import android.security.keymaster.KeymasterDefs; -import android.security.keystore.KeyProperties; import java.security.AlgorithmParameters; import java.security.NoSuchAlgorithmException; -import java.security.ProviderException; import java.security.spec.ECGenParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.InvalidParameterSpecException; @@ -30,7 +27,7 @@ import java.security.spec.InvalidParameterSpecException; /** * @hide */ -public abstract class KeymasterUtils { +public final class KeymasterUtils { private KeymasterUtils() {} @@ -86,47 +83,6 @@ public abstract class KeymasterUtils { } } - /** - * Adds {@code KM_TAG_MIN_MAC_LENGTH} tag, if necessary, to the keymaster arguments for - * generating or importing a key. This tag may only be needed for symmetric keys (e.g., HMAC, - * AES-GCM). - */ - public static void addMinMacLengthAuthorizationIfNecessary(KeymasterArguments args, - int keymasterAlgorithm, - int[] keymasterBlockModes, - int[] keymasterDigests) { - switch (keymasterAlgorithm) { - case KeymasterDefs.KM_ALGORITHM_AES: - if (com.android.internal.util.ArrayUtils.contains( - keymasterBlockModes, KeymasterDefs.KM_MODE_GCM)) { - // AES GCM key needs the minimum length of AEAD tag specified. - args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, - AndroidKeyStoreAuthenticatedAESCipherSpi.GCM - .MIN_SUPPORTED_TAG_LENGTH_BITS); - } - break; - case KeymasterDefs.KM_ALGORITHM_HMAC: - // HMAC key needs the minimum length of MAC set to the output size of the associated - // digest. This is because we do not offer a way to generate shorter MACs and - // don't offer a way to verify MACs (other than by generating them). - if (keymasterDigests.length != 1) { - throw new ProviderException( - "Unsupported number of authorized digests for HMAC key: " - + keymasterDigests.length - + ". Exactly one digest must be authorized"); - } - int keymasterDigest = keymasterDigests[0]; - int digestOutputSizeBits = getDigestOutputSizeBits(keymasterDigest); - if (digestOutputSizeBits == -1) { - throw new ProviderException( - "HMAC key authorized for unsupported digest: " - + KeyProperties.Digest.fromKeymaster(keymasterDigest)); - } - args.addUnsignedInt(KeymasterDefs.KM_TAG_MIN_MAC_LENGTH, digestOutputSizeBits); - break; - } - } - static String getEcCurveFromKeymaster(int ecCurve) { switch (ecCurve) { case android.hardware.security.keymint.EcCurve.P_224: diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS index ab2f3ef94eb6..68970e68de07 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -3,5 +3,5 @@ pbdr@google.com pragyabajoria@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com, peanutbutter@google.com, jeremysim@google.com per-file res*/*/tv_*.xml = bronger@google.com diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 8d7e5fd95957..d50a14cf5dae 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -18,23 +18,28 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" android:layout_width="wrap_content" - android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:layout_height="wrap_content" android:background="@drawable/desktop_mode_maximize_menu_background" android:elevation="1dp"> <LinearLayout android:id="@+id/container" android:layout_width="wrap_content" - android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:layout_height="wrap_content" android:orientation="horizontal" - android:padding="16dp" + android:paddingHorizontal="12dp" + android:paddingVertical="16dp" + android:measureWithLargestChild="true" android:gravity="center"> <LinearLayout android:id="@+id/maximize_menu_immersive_toggle_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <Button android:layout_width="94dp" @@ -44,21 +49,22 @@ android:stateListAnimator="@null" android:importantForAccessibility="yes" android:contentDescription="@string/desktop_mode_maximize_menu_immersive_button_text" - android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" android:alpha="0"/> <TextView android:id="@+id/maximize_menu_immersive_toggle_button_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" + android:lineHeight="16sp" android:gravity="center" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:importantForAccessibility="no" android:text="@string/desktop_mode_maximize_menu_immersive_button_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> @@ -66,7 +72,11 @@ android:id="@+id/maximize_menu_size_toggle_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_horizontal" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <Button android:layout_width="94dp" @@ -81,15 +91,17 @@ <TextView android:id="@+id/maximize_menu_size_toggle_button_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" + android:lineHeight="16sp" android:gravity="center" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:importantForAccessibility="no" android:text="@string/desktop_mode_maximize_menu_maximize_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> @@ -97,7 +109,11 @@ android:id="@+id/maximize_menu_snap_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_horizontal" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <LinearLayout android:id="@+id/maximize_menu_snap_menu_layout" android:layout_width="wrap_content" @@ -106,7 +122,6 @@ android:padding="4dp" android:background="@drawable/desktop_mode_maximize_menu_layout_background" android:layout_marginBottom="4dp" - android:layout_marginStart="8dp" android:alpha="0"> <Button android:id="@+id/maximize_menu_snap_left_button" @@ -131,16 +146,17 @@ </LinearLayout> <TextView android:id="@+id/maximize_menu_snap_window_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" - android:layout_gravity="center" + android:lineHeight="16sp" android:gravity="center" android:importantForAccessibility="no" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:text="@string/desktop_mode_maximize_menu_snap_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> </LinearLayout> @@ -150,6 +166,6 @@ <View android:id="@+id/maximize_menu_overlay" android:layout_width="match_parent" - android:layout_height="@dimen/desktop_mode_maximize_menu_height"/> + android:layout_height="match_parent"/> </FrameLayout> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index e395341a5792..f5f3f0fe52eb 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -498,14 +498,6 @@ <!-- The default minimum allowed window height when resizing a window in desktop mode. --> <dimen name="desktop_mode_minimum_window_height">352dp</dimen> - <!-- The width of the maximize menu in desktop mode, depending on the number of options --> - <dimen name="desktop_mode_maximize_menu_width_one_options">126dp</dimen> - <dimen name="desktop_mode_maximize_menu_width_two_options">228dp</dimen> - <dimen name="desktop_mode_maximize_menu_width_three_options">330dp</dimen> - - <!-- The height of the maximize menu in desktop mode. --> - <dimen name="desktop_mode_maximize_menu_height">114dp</dimen> - <!-- The padding of the maximize menu in desktop mode. --> <dimen name="desktop_mode_menu_padding">16dp</dimen> diff --git a/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml new file mode 100644 index 000000000000..975d25b25953 --- /dev/null +++ b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:alpha="0.35" android:color="@androidprv:color/materialColorPrimaryContainer" /> +</selector> diff --git a/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml new file mode 100644 index 000000000000..89546f9b0807 --- /dev/null +++ b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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"> + <corners android:radius="28dp" /> + <solid android:color="@color/bubble_drop_target_background_color" /> + <stroke + android:width="1dp" + android:color="@androidprv:color/materialColorPrimaryContainer" /> +</shape> diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index d280083ae7f5..5f013c52d70d 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -36,4 +36,13 @@ <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen> <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen> <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> + + <!-- Bubble drop target dimensions --> + <dimen name="drop_target_full_screen_padding">20dp</dimen> + <dimen name="drop_target_desktop_window_padding_small">100dp</dimen> + <dimen name="drop_target_desktop_window_padding_large">130dp</dimen> + <dimen name="drop_target_expanded_view_width">364</dimen> + <dimen name="drop_target_expanded_view_height">578</dimen> + <dimen name="drop_target_expanded_view_padding_bottom">108</dimen> + <dimen name="drop_target_expanded_view_padding_horizontal">24</dimen> </resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index 4d00c74155a8..851987269c10 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -21,6 +21,7 @@ import static android.view.RemoteAnimationTarget.MODE_CHANGING; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; +import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; @@ -55,9 +56,15 @@ import java.util.function.Predicate; public class TransitionUtil { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; + public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1; /** Flag applied to a transition change to identify it as a desktop wallpaper activity. */ - public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 1; + public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2; + + /** + * Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher. + */ + public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1; /** @return true if the transition was triggered by opening something vs closing something */ public static boolean isOpeningType(@WindowManager.TransitionType int type) { @@ -117,6 +124,11 @@ public class TransitionUtil { return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR); } + /** Returns `true` if `change` is an app's dim layer. */ + public static boolean isDimLayer(TransitionInfo.Change change) { + return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER); + } + /** Returns `true` if `change` is only re-ordering. */ public static boolean isOrderOnly(TransitionInfo.Change change) { return change.getMode() == TRANSIT_CHANGE @@ -231,6 +243,14 @@ public class TransitionUtil { t.setLayer(leash, Integer.MAX_VALUE); return; } + if (isDimLayer(change)) { + // When a dim layer gets reparented onto the transition root, we need to zero out its + // position so that it's in line with everything else on the transition root. Also, + // we need to set a crop because we don't want it applying MATCH_PARENT on the whole + // root surface. + t.setPosition(leash, 0, 0); + t.setCrop(leash, change.getEndAbsBounds()); + } // Put all the OPEN/SHOW on top if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { @@ -284,14 +304,19 @@ public class TransitionUtil { // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) setupLeash(leashSurface, change, info.getChanges().size() - order, info, t); t.reparent(change.getLeash(), leashSurface); - t.setAlpha(change.getLeash(), 1.0f); - t.show(change.getLeash()); + if (!isDimLayer(change)) { + // Most leashes going onto the transition root should have their alpha set here to make + // them visible. But dim layers should be left untouched (their alpha value is their + // actual dim value). + t.setAlpha(change.getLeash(), 1.0f); + } if (!isDividerBar(change)) { // For divider, don't modify its inner leash position when creating the outer leash // for the transition. In case the position being wrong after the transition finished. t.setPosition(change.getLeash(), 0, 0); } t.setLayer(change.getLeash(), 0); + t.show(change.getLeash()); return leashSurface; } @@ -333,6 +358,9 @@ public class TransitionUtil { if (isDividerBar(change)) { return getDividerTarget(change, leash); } + if (isDimLayer(change)) { + return getDimLayerTarget(change, leash); + } int taskId; boolean isNotInRecents; @@ -439,6 +467,17 @@ public class TransitionUtil { TYPE_DOCK_DIVIDER); } + private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change, + SurfaceControl leash) { + return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), + leash, false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(), + change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */, + null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */, + TYPE_SPLIT_SCREEN_DIM_LAYER); + } + /** * Finds the "correct" root idx for a change. The change's end display is prioritized, then * the start display. If there is no display, it will fallback on the 0th root in the diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java index f45dc3a1e892..e92c1eb81e89 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java @@ -93,10 +93,21 @@ public class Interpolators { public static final PathInterpolator SLOWDOWN_INTERPOLATOR = new PathInterpolator(0.5f, 1f, 0.5f, 1f); + /** + * An interpolator used for dimming a task as it travels offscreen, or towards a distant dismiss + * point. A sharp rise, followed by a steady middle, and ending with another sharp rise. + */ public static final PathInterpolator DIM_INTERPOLATOR = new PathInterpolator(.23f, .87f, .52f, -0.11f); /** + * An interpolator used for dimming a task very quickly. Roughly approximates one of the "sharp + * rises" of {@link #DIM_INTERPOLATOR}. + */ + public static final PathInterpolator FAST_DIM_INTERPOLATOR = + new PathInterpolator(0.23f, 0.87f, 0.83f, 0.83f); + + /** * Use this interpolator for animating progress values coming from the back callback to get * the predictive-back-typical decelerate motion. * diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt index 481fc7fcb869..6acd9dbe8b91 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt @@ -70,7 +70,8 @@ enum class BubbleBarLocation : Parcelable { UpdateSource.A11Y_ACTION_BAR, UpdateSource.A11Y_ACTION_BUBBLE, UpdateSource.A11Y_ACTION_EXP_VIEW, - UpdateSource.APP_ICON_DRAG + UpdateSource.APP_ICON_DRAG, + UpdateSource.DRAG_TASK, ) @Retention(AnnotationRetention.SOURCE) annotation class UpdateSource { @@ -95,6 +96,9 @@ enum class BubbleBarLocation : Parcelable { /** Location changed from dragging the application icon to the bubble bar */ const val APP_ICON_DRAG = 7 + + /** Location changed from dragging a running task to the bubble bar */ + const val DRAG_TASK = 8 } } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt index 909e9d2c4428..1a80b0f29aa9 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.shared.bubbles import android.content.Context import android.graphics.Rect +import android.util.TypedValue import androidx.annotation.DimenRes import com.android.wm.shell.shared.R import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode @@ -50,6 +51,60 @@ class DragZoneFactory( private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0 private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0 + private var fullScreenDropTargetPadding = 0 + private var desktopWindowDropTargetPaddingSmall = 0 + private var desktopWindowDropTargetPaddingLarge = 0 + private var expandedViewDropTargetWidth = 0 + private var expandedViewDropTargetHeight = 0 + private var expandedViewDropTargetPaddingBottom = 0 + private var expandedViewDropTargetPaddingHorizontal = 0 + + private val fullScreenDropTarget: Rect + get() = + Rect(windowBounds).apply { + inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding) + } + + private val desktopWindowDropTarget: Rect + get() = + Rect(windowBounds).apply { + if (deviceConfig.isLandscape) { + inset( + /* dx= */ desktopWindowDropTargetPaddingLarge, + /* dy= */ desktopWindowDropTargetPaddingSmall + ) + } else { + inset( + /* dx= */ desktopWindowDropTargetPaddingSmall, + /* dy= */ desktopWindowDropTargetPaddingLarge + ) + } + } + + private val expandedViewDropTargetLeft: Rect + get() = + Rect( + expandedViewDropTargetPaddingHorizontal, + windowBounds.bottom - + expandedViewDropTargetPaddingBottom - + expandedViewDropTargetHeight, + expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal, + windowBounds.bottom - expandedViewDropTargetPaddingBottom + ) + + private val expandedViewDropTargetRight: Rect + get() = + Rect( + windowBounds.right - + expandedViewDropTargetPaddingHorizontal - + expandedViewDropTargetWidth, + windowBounds.bottom - + expandedViewDropTargetPaddingBottom - + expandedViewDropTargetHeight, + windowBounds.right - expandedViewDropTargetPaddingHorizontal, + windowBounds.bottom - expandedViewDropTargetPaddingBottom + ) + init { onConfigurationUpdated() } @@ -88,11 +143,32 @@ class DragZoneFactory( context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall) vSplitFromExpandedViewDragZoneHeightFoldShort = context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short) + fullScreenDropTargetPadding = + context.resolveDimension(R.dimen.drop_target_full_screen_padding) + desktopWindowDropTargetPaddingSmall = + context.resolveDimension(R.dimen.drop_target_desktop_window_padding_small) + desktopWindowDropTargetPaddingLarge = + context.resolveDimension(R.dimen.drop_target_desktop_window_padding_large) + + // TODO b/393172431: Use the shared xml resources once we can easily access them from + // launcher + expandedViewDropTargetWidth = 364.dpToPx() + expandedViewDropTargetHeight = 578.dpToPx() + expandedViewDropTargetPaddingBottom = 108.dpToPx() + expandedViewDropTargetPaddingHorizontal = 24.dpToPx() } private fun Context.resolveDimension(@DimenRes dimension: Int) = resources.getDimensionPixelSize(dimension) + private fun Int.dpToPx() = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + context.resources.displayMetrics + ) + .toInt() + /** * Creates the list of drag zones for the dragged object. * @@ -155,7 +231,7 @@ class DragZoneFactory( DragZone.Bubble.Left( bounds = Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom), - dropTarget = Rect(0, 0, 0, 0), + dropTarget = expandedViewDropTargetLeft, ), DragZone.Bubble.Right( bounds = @@ -165,7 +241,7 @@ class DragZoneFactory( windowBounds.right, windowBounds.bottom, ), - dropTarget = Rect(0, 0, 0, 0), + dropTarget = expandedViewDropTargetRight, ) ) } @@ -174,7 +250,7 @@ class DragZoneFactory( return listOf( DragZone.Bubble.Left( bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom), - dropTarget = Rect(0, 0, 0, 0), + dropTarget = expandedViewDropTargetLeft, ), DragZone.Bubble.Right( bounds = @@ -184,7 +260,7 @@ class DragZoneFactory( windowBounds.right, windowBounds.bottom, ), - dropTarget = Rect(0, 0, 0, 0), + dropTarget = expandedViewDropTargetRight, ) ) } @@ -198,7 +274,7 @@ class DragZoneFactory( windowBounds.right / 2 + fullScreenDragZoneWidth / 2, fullScreenDragZoneHeight ), - dropTarget = Rect(0, 0, 0, 0) + dropTarget = fullScreenDropTarget ) } @@ -223,7 +299,7 @@ class DragZoneFactory( windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2 ) }, - dropTarget = Rect(0, 0, 0, 0) + dropTarget = desktopWindowDropTarget ) } @@ -236,7 +312,7 @@ class DragZoneFactory( windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2, windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2 ), - dropTarget = Rect(0, 0, 0, 0) + dropTarget = desktopWindowDropTarget ) } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 643c1506e4c2..00c446c3da60 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -27,6 +27,7 @@ import android.hardware.display.DisplayManager; import android.os.SystemProperties; import android.view.Display; import android.view.WindowManager; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.R; @@ -226,6 +227,7 @@ public class DesktopModeStatus { return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); } + /** * Return {@code true} if desktop mode dev option should be shown on current device */ @@ -239,23 +241,22 @@ public class DesktopModeStatus { */ public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) { return Flags.showDesktopExperienceDevOption() - && isInternalDisplayEligibleToHostDesktops(context); + && isDeviceEligibleForDesktopMode(context); } /** Returns if desktop mode dev option should be enabled if there is no user override. */ public static boolean shouldDevOptionBeEnabledByDefault(Context context) { - return isInternalDisplayEligibleToHostDesktops(context) - && Flags.enableDesktopWindowingMode(); + return isDeviceEligibleForDesktopMode(context) + && Flags.enableDesktopWindowingMode(); } /** * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - return (isInternalDisplayEligibleToHostDesktops(context) - && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() - && (isDesktopModeSupported(context) || !enforceDeviceRestrictions()) - || isDesktopModeEnabledByDevOption(context)); + return (isDeviceEligibleForDesktopMode(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) + || isDesktopModeEnabledByDevOption(context); } /** @@ -271,7 +272,7 @@ public class DesktopModeStatus { * frontend implementations). */ public static boolean enableMultipleDesktops(@NonNull Context context) { - return Flags.enableMultipleDesktopsBackend() + return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() && Flags.enableMultipleDesktopsFrontend() && canEnterDesktopMode(context); } @@ -323,25 +324,34 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop sessions is unrestricted and can be host for the device's - * internal display. + * Return {@code true} if desktop mode is unrestricted and is supported on the device. */ - public static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { - return !enforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( - Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionSupported( - context)); + public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { + if (!enforceDeviceRestrictions()) { + return true; + } + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); + final boolean desktopModeSupportedByDevOptions = + Flags.enableDesktopModeThroughDevOption() + && isDesktopModeDevOptionSupported(context); + return desktopModeSupported || desktopModeSupportedByDevOptions; } /** * Return {@code true} if the developer option for desktop mode is unrestricted and is supported * in the device. * - * Note that, if {@link #isInternalDisplayEligibleToHostDesktops(Context)} is true, then + * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then * {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true. */ private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) { - return !enforceDeviceRestrictions() || isDesktopModeSupported(context) - || isDesktopModeDevOptionSupported(context); + if (!enforceDeviceRestrictions()) { + return true; + } + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); + return desktopModeSupported || isDesktopModeDevOptionSupported(context); } /** diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java index b48296f5f76a..759e711100c3 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java @@ -262,6 +262,7 @@ public class SplitScreenConstants { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR; + public static final int FLAG_IS_DIM_LAYER = TransitionUtil.FLAG_IS_DIM_LAYER; public static final String splitPositionToString(@SplitPosition int pos) { switch (pos) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index f51023fcaaf5..58b46d202599 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -845,6 +845,10 @@ public class BubbleController implements ConfigurationChangeListener, mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_APP_ICON_DROP : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_APP_ICON_DROP); break; + case BubbleBarLocation.UpdateSource.DRAG_TASK: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_TASK + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_TASK); + break; } } @@ -1291,6 +1295,11 @@ public class BubbleController implements ConfigurationChangeListener, mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.importance_ring_stroke_width)); mStackView.onDisplaySizeChanged(); + // TODO b/392893178: Merge the unfold and the task view transition so that we don't + // have to post a delayed runnable to the looper to update the bounds + if (mStackView.isExpanded()) { + mStackView.postDelayed(() -> mStackView.updateExpandedView(), 500); + } } if (newConfig.fontScale != mFontScale) { mFontScale = newConfig.fontScale; @@ -1598,13 +1607,21 @@ public class BubbleController implements ConfigurationChangeListener, if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return; Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId); + BubbleBarLocation location = null; + if (dragData != null) { + location = + dragData.isReleasedOnLeft() ? BubbleBarLocation.LEFT : BubbleBarLocation.RIGHT; + } if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, location); if (dragData != null && dragData.getPendingWct() != null) { mTransitions.startTransition(TRANSIT_CHANGE, dragData.getPendingWct(), /* handler= */ null); } } else { + if (location != null) { + setBubbleBarLocation(location, BubbleBarLocation.UpdateSource.DRAG_TASK); + } b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java index 831f2271d500..a0c473173bf1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java @@ -156,6 +156,12 @@ public class BubbleLogger { @UiEvent(doc = "while bubble bar is expanded, switch to another/existing bubble") BUBBLE_BAR_BUBBLE_SWITCHED(1977), + @UiEvent(doc = "bubble bar moved to the left edge of the screen by dragging a task") + BUBBLE_BAR_MOVED_LEFT_DRAG_TASK(2146), + + @UiEvent(doc = "bubble bar moved to the right edge of the screen by dragging a task") + BUBBLE_BAR_MOVED_RIGHT_DRAG_TASK(2147), + // endregion ; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index dad627f85d95..92724178cf84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -3548,7 +3548,7 @@ public class BubbleStackView extends FrameLayout } } - private void updateExpandedView() { + void updateExpandedView() { boolean isOverflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()); int[] paddings = mPositioner.getExpandedViewContainerPadding( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index 6be3c1f18b39..a676f41baafe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -156,14 +156,18 @@ public class BubbleTransitions { public static class DragData { private final Rect mBounds; private final WindowContainerTransaction mPendingWct; + private final boolean mReleasedOnLeft; /** * @param bounds bounds of the dragged task when the drag was released * @param wct pending operations to be applied when finishing the drag + * @param releasedOnLeft true if the bubble was released in the left drop target */ - public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct) { + public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct, + boolean releasedOnLeft) { mBounds = bounds; mPendingWct = wct; + mReleasedOnLeft = releasedOnLeft; } /** @@ -181,6 +185,13 @@ public class BubbleTransitions { public WindowContainerTransaction getPendingWct() { return mPendingWct; } + + /** + * @return true if the bubble was released in the left drop target + */ + public boolean isReleasedOnLeft() { + return mReleasedOnLeft; + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index 4c3bde9b2b3a..97184c859d4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -67,6 +67,7 @@ public class DisplayController { private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); private final Map<Integer, RectF> mUnpopulatedDisplayBounds = new HashMap<>(); + private DisplayTopology mDisplayTopology; public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, ShellExecutor mainExecutor, DisplayManager displayManager) { @@ -157,6 +158,7 @@ public class DisplayController { for (int i = 0; i < mDisplays.size(); ++i) { listener.onDisplayAdded(mDisplays.keyAt(i)); } + listener.onTopologyChanged(mDisplayTopology); } } @@ -245,6 +247,7 @@ public class DisplayController { if (topology == null) { return; } + mDisplayTopology = topology; SparseArray<RectF> absoluteBounds = topology.getAbsoluteBounds(); mUnpopulatedDisplayBounds.clear(); for (int i = 0; i < absoluteBounds.size(); ++i) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt new file mode 100644 index 000000000000..a1d700af5569 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.common + +import android.window.WindowContainerTransaction +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<WindowContainerTransaction>]. This can be used in place of kotlin default + * parameters values [builder = ::WindowContainerTransaction] which requires the + * [@JvmOverloads] annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [WindowContainerTransaction]s. + */ +@WMSingleton +class WindowContainerTransactionSupplier @Inject constructor( +) : Supplier<WindowContainerTransaction> { + override fun get(): WindowContainerTransaction = WindowContainerTransaction() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java new file mode 100644 index 000000000000..fb2a324375b6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 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.common.split; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when + * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_ALIGN_CENTER} is the desired + * parallax effect. + */ +public class CenterParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + if (isLeftRightSplit) { + retreatingOut.x = (retreatingSurface.width() - retreatingContent.width()) / 2; + } else { + retreatingOut.y = (retreatingSurface.height() - retreatingContent.height()) / 2; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java new file mode 100644 index 000000000000..39ecbb379d7d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 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.common.split; + +import static android.view.WindowManager.DOCKED_INVALID; + +import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; +import android.view.WindowManager; + +/** + * Calculation class, used when + * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_DISMISSING} is the desired parallax + * effect. + */ +public class DismissingParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + if (dimmingSide == DOCKED_INVALID) { + return; + } + + float progressTowardScreenEdge = + Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); + int totalDismissingDistance = 0; + if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) { + totalDismissingDistance = snapAlgorithm.getDismissStartTarget().getPosition() + - snapAlgorithm.getFirstSplitTarget().getPosition(); + } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) { + totalDismissingDistance = snapAlgorithm.getLastSplitTarget().getPosition() + - snapAlgorithm.getDismissEndTarget().getPosition(); + } + + float parallaxFraction = + calculateParallaxDismissingFraction(progressTowardScreenEdge, dimmingSide); + if (isLeftRightSplit) { + retreatingOut.x = (int) (parallaxFraction * totalDismissingDistance); + } else { + retreatingOut.y = (int) (parallaxFraction * totalDismissingDistance); + } + } + + /** + * @return for a specified {@code fraction}, this returns an adjusted value that simulates a + * slowing down parallax effect + */ + private float calculateParallaxDismissingFraction(float fraction, int dockSide) { + float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; + + // Less parallax at the top, just because. + if (dockSide == WindowManager.DOCKED_TOP) { + result /= 2f; + } + return result; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index 2f5afcaa907b..5b2dd97a338f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -465,5 +465,9 @@ public class DividerSnapAlgorithm { this.snapPosition = snapPosition; this.distanceMultiplier = distanceMultiplier; } + + public int getPosition() { + return position; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java new file mode 100644 index 000000000000..9fa162164e0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 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.common.split; + +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM; +import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.shared.animation.Interpolators.FAST_DIM_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_FLEX} + * is the desired parallax effect. + */ +public class FlexParallaxSpec implements ParallaxSpec { + final Rect mTempRect = new Rect(); + + @Override + public int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, + boolean isLeftRightSplit) { + if (position < snapAlgorithm.getMiddleTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; + } else if (position > snapAlgorithm.getMiddleTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; + } + return DOCKED_INVALID; + } + + /** + * Calculates the amount of dim to apply to a task surface moving offscreen in flexible split. + * In flexible split, there are two dimming "behaviors". + * 1) "slow dim": when moving the divider from the middle of the screen to a target at 10% or + * 90%, we dim the app slightly as it moves partially offscreen. + * 2) "fast dim": when moving the divider from a side snap target further toward the screen + * edge, we dim the app rapidly as it approaches the dismiss point. + * @return 0f = no dim applied. 1f = full black. + */ + public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { + int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition(); + int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition(); + int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition(); + int lastTargetPos = snapAlgorithm.getLastSplitTarget().getPosition(); + int endDismissPos = snapAlgorithm.getDismissEndTarget().getPosition(); + float progress; + + if (startDismissPos <= position && position < firstTargetPos) { + // Divider is on the left/top (between 0% and 10% of screen), "fast dim" as it moves + // toward the screen edge + progress = (float) (firstTargetPos - position) / (firstTargetPos - startDismissPos); + return fastDim(progress); + } else if (firstTargetPos <= position && position < middleTargetPos) { + // Divider is between 10% and 50%, "slow dim" as it moves toward the left/top target + progress = (float) (middleTargetPos - position) / (middleTargetPos - firstTargetPos); + return slowDim(progress); + } else if (middleTargetPos <= position && position < lastTargetPos) { + // Divider is between 50% and 90%, "slow dim" as it moves toward the right/bottom target + progress = (float) (position - middleTargetPos) / (lastTargetPos - middleTargetPos); + return slowDim(progress); + } else if (lastTargetPos <= position && position <= endDismissPos) { + // Divider is on the right/bottom (between 90% and 100% of screen), "fast dim" as it + // moves toward screen edge + progress = (float) (position - lastTargetPos) / (endDismissPos - lastTargetPos); + return fastDim(progress); + } + return 0f; + } + + /** + * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at zero and ramps + * up to the default amount of dimming for an offscreen app, + * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM}. + */ + private float slowDim(float progress) { + return DIM_INTERPOLATOR.getInterpolation(progress) * DEFAULT_OFFSCREEN_DIM; + } + + /** + * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at + * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM} and ramps up to 100% dim (full black). + */ + private float fastDim(float progress) { + return DEFAULT_OFFSCREEN_DIM + (FAST_DIM_INTERPOLATOR.getInterpolation(progress) + * (1 - DEFAULT_OFFSCREEN_DIM)); + } + + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + // Whether an app is getting pushed offscreen by the divider. + boolean isRetreatingOffscreen = !displayBounds.contains(retreatingSurface); + // Whether an app was getting pulled onscreen at the beginning of the drag. + boolean advancingSideStartedOffscreen = !displayBounds.contains(advancingContent); + + // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10) + if (isRetreatingOffscreen && !advancingSideStartedOffscreen) { + // On the left side, we use parallax to simulate the contents sticking to the + // divider. This is because surfaces naturally expand to the bottom and right, + // so when a surface's area expands, the contents stick to the left. This is + // correct behavior on the right-side surface, but not the left. + if (topLeftShrink) { + if (isLeftRightSplit) { + retreatingOut.x = retreatingSurface.width() - retreatingContent.width(); + } else { + retreatingOut.y = retreatingSurface.height() - retreatingContent.height(); + } + } + // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss) + } else { + mTempRect.set(retreatingSurface); + Point rootOffset = new Point(); + // 10:90 -> 50:50, 10:90, or dismiss right + if (advancingSideStartedOffscreen) { + // We have to handle a complicated case here to keep the parallax smooth. + // When the divider crosses the 50% mark, the retreating-side app surface + // will start expanding offscreen. This is expected and unavoidable, but + // makes the parallax look disjointed. In order to preserve the illusion, + // we add another offset (rootOffset) to simulate the surface staying + // onscreen. + if (mTempRect.intersect(displayBounds)) { + if (retreatingSurface.left < displayBounds.left) { + rootOffset.x = displayBounds.left - retreatingSurface.left; + } + if (retreatingSurface.top < displayBounds.top) { + rootOffset.y = displayBounds.top - retreatingSurface.top; + } + } + + // On the left side, we again have to simulate the contents sticking to the + // divider. + if (!topLeftShrink) { + if (isLeftRightSplit) { + advancingOut.x = advancingSurface.width() - advancingContent.width(); + } else { + advancingOut.y = advancingSurface.height() - advancingContent.height(); + } + } + } + + // In all these cases, the shrinking app also receives a center parallax. + if (isLeftRightSplit) { + retreatingOut.x = rootOffset.x + + ((mTempRect.width() - retreatingContent.width()) / 2); + } else { + retreatingOut.y = rootOffset.y + + ((mTempRect.height() - retreatingContent.height()) / 2); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java new file mode 100644 index 000000000000..043b2880f28b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 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.common.split; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_NONE} + * is the desired parallax effect. + */ +public class NoParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + // no-op + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java new file mode 100644 index 000000000000..84d849b3c1f9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java @@ -0,0 +1,62 @@ +/* + * 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.common.split; + +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Default interface for a set of calculation classes, used for calculating various parallax and + * dimming effects in split screen. + */ +public interface ParallaxSpec { + /** Returns an int indicating which side of the screen is being dimmed (if any). */ + default int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, + boolean isLeftRightSplit) { + if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; + } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; + } + return DOCKED_INVALID; + } + + /** Returns the dim amount that we'll apply to the app surface. 0f = no dim, 1f = full black */ + default float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { + float progressTowardScreenEdge = + Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); + return DIM_INTERPOLATOR.getInterpolation(progressTowardScreenEdge); + } + + /** + * Calculates the amount to offset app surfaces to create nice parallax effects. Writes to + * {@link ResizingEffectPolicy#mRetreatingSideParallax} and + * {@link ResizingEffectPolicy#mAdvancingSideParallax}. + */ + void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java index 3f76fd0220ff..e2e1f9698a90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java @@ -26,27 +26,32 @@ import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTE import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_DISMISSING; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_NONE; -import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; -import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import android.graphics.Point; import android.graphics.Rect; import android.view.SurfaceControl; -import android.view.WindowManager; /** * This class governs how and when parallax and dimming effects are applied to task surfaces, * usually when the divider is being moved around by the user (or during an animation). */ class ResizingEffectPolicy { + /** The default amount to dim an app that is partially offscreen. */ + public static float DEFAULT_OFFSCREEN_DIM = 0.32f; + private final SplitLayout mSplitLayout; /** The parallax algorithm we are currently using. */ private final int mParallaxType; + /** + * A convenience class, corresponding to {@link #mParallaxType}, that performs all the + * calculations for parallax and dimming values. + */ + private final ParallaxSpec mParallaxSpec; int mShrinkSide = DOCKED_INVALID; // The current dismissing side. - int mDismissingSide = DOCKED_INVALID; + int mDimmingSide = DOCKED_INVALID; /** * A {@link Point} that stores a single x and y value, representing the parallax translation @@ -62,7 +67,7 @@ class ResizingEffectPolicy { final Point mAdvancingSideParallax = new Point(); // The dimming value to hint the dismissing side and progress. - float mDismissingDimValue = 0.0f; + float mDimValue = 0.0f; /** * Content bounds for the app that the divider is moving toward. This is the content that is @@ -95,35 +100,38 @@ class ResizingEffectPolicy { ResizingEffectPolicy(int parallaxType, SplitLayout splitLayout) { mParallaxType = parallaxType; mSplitLayout = splitLayout; + switch (mParallaxType) { + case PARALLAX_DISMISSING: + mParallaxSpec = new DismissingParallaxSpec(); + break; + case PARALLAX_ALIGN_CENTER: + mParallaxSpec = new CenterParallaxSpec(); + break; + case PARALLAX_FLEX: + mParallaxSpec = new FlexParallaxSpec(); + break; + case PARALLAX_NONE: + default: + mParallaxSpec = new NoParallaxSpec(); + break; + } } /** - * Calculates the desired parallax values and stores them in {@link #mRetreatingSideParallax} - * and {@link #mAdvancingSideParallax}. These values will be then be applied in - * {@link #adjustRootSurface}. - * - * @param position The divider's position on the screen (x-coordinate in left-right split, - * y-coordinate in top-bottom split). + * Calculates the desired parallax and dimming values for a task surface and stores them in + * {@link #mRetreatingSideParallax}, {@link #mAdvancingSideParallax}, and + * {@link #mDimValue} These values will be then be applied in + * {@link #adjustRootSurface} and {@link #adjustDimSurface} respectively. */ void applyDividerPosition( int position, boolean isLeftRightSplit, DividerSnapAlgorithm snapAlgorithm) { - mDismissingSide = DOCKED_INVALID; + mDimmingSide = DOCKED_INVALID; mRetreatingSideParallax.set(0, 0); mAdvancingSideParallax.set(0, 0); - mDismissingDimValue = 0; + mDimValue = 0; Rect displayBounds = mSplitLayout.getRootBounds(); - int totalDismissingDistance = 0; - if (position < snapAlgorithm.getFirstSplitTarget().position) { - mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; - totalDismissingDistance = snapAlgorithm.getDismissStartTarget().position - - snapAlgorithm.getFirstSplitTarget().position; - } else if (position > snapAlgorithm.getLastSplitTarget().position) { - mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; - totalDismissingDistance = snapAlgorithm.getLastSplitTarget().position - - snapAlgorithm.getDismissEndTarget().position; - } - + // Figure out which side is shrinking, and assign retreating/advancing bounds final boolean topLeftShrink = isLeftRightSplit ? position < mSplitLayout.getTopLeftContentBounds().right : position < mSplitLayout.getTopLeftContentBounds().bottom; @@ -141,106 +149,20 @@ class ResizingEffectPolicy { mAdvancingSurface.set(mSplitLayout.getTopLeftBounds()); } - if (mDismissingSide != DOCKED_INVALID) { - float fraction = - Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); - mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); - if (mParallaxType == PARALLAX_DISMISSING) { - fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); - if (isLeftRightSplit) { - mRetreatingSideParallax.x = (int) (fraction * totalDismissingDistance); - } else { - mRetreatingSideParallax.y = (int) (fraction * totalDismissingDistance); - } - } - } - - if (mParallaxType == PARALLAX_ALIGN_CENTER) { - if (isLeftRightSplit) { - mRetreatingSideParallax.x = - (mRetreatingSurface.width() - mRetreatingContent.width()) / 2; - } else { - mRetreatingSideParallax.y = - (mRetreatingSurface.height() - mRetreatingContent.height()) / 2; - } - } else if (mParallaxType == PARALLAX_FLEX) { - // Whether an app is getting pushed offscreen by the divider. - boolean isRetreatingOffscreen = !displayBounds.contains(mRetreatingSurface); - // Whether an app was getting pulled onscreen at the beginning of the drag. - boolean advancingSideStartedOffscreen = !displayBounds.contains(mAdvancingContent); + // Figure out if we should be dimming one side + mDimmingSide = mParallaxSpec.getDimmingSide(position, snapAlgorithm, isLeftRightSplit); - // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10) - if (isRetreatingOffscreen && !advancingSideStartedOffscreen) { - // On the left side, we use parallax to simulate the contents sticking to the - // divider. This is because surfaces naturally expand to the bottom and right, - // so when a surface's area expands, the contents stick to the left. This is - // correct behavior on the right-side surface, but not the left. - if (topLeftShrink) { - if (isLeftRightSplit) { - mRetreatingSideParallax.x = - mRetreatingSurface.width() - mRetreatingContent.width(); - } else { - mRetreatingSideParallax.y = - mRetreatingSurface.height() - mRetreatingContent.height(); - } - } - // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss) - } else { - mTempRect.set(mRetreatingSurface); - Point rootOffset = new Point(); - // 10:90 -> 50:50, 10:90, or dismiss right - if (advancingSideStartedOffscreen) { - // We have to handle a complicated case here to keep the parallax smooth. - // When the divider crosses the 50% mark, the retreating-side app surface - // will start expanding offscreen. This is expected and unavoidable, but - // makes the parallax look disjointed. In order to preserve the illusion, - // we add another offset (rootOffset) to simulate the surface staying - // onscreen. - mTempRect.intersect(displayBounds); - if (mRetreatingSurface.left < displayBounds.left) { - rootOffset.x = displayBounds.left - mRetreatingSurface.left; - } - if (mRetreatingSurface.top < displayBounds.top) { - rootOffset.y = displayBounds.top - mRetreatingSurface.top; - } - - // On the left side, we again have to simulate the contents sticking to the - // divider. - if (!topLeftShrink) { - if (isLeftRightSplit) { - mAdvancingSideParallax.x = - mAdvancingSurface.width() - mAdvancingContent.width(); - } else { - mAdvancingSideParallax.y = - mAdvancingSurface.height() - mAdvancingContent.height(); - } - } - } - - // In all these cases, the shrinking app also receives a center parallax. - if (isLeftRightSplit) { - mRetreatingSideParallax.x = rootOffset.x - + ((mTempRect.width() - mRetreatingContent.width()) / 2); - } else { - mRetreatingSideParallax.y = rootOffset.y - + ((mTempRect.height() - mRetreatingContent.height()) / 2); - } - } + // If so, calculate dimming + if (mDimmingSide != DOCKED_INVALID) { + mDimValue = mParallaxSpec.getDimValue(position, snapAlgorithm); } - } - /** - * @return for a specified {@code fraction}, this returns an adjusted value that simulates a - * slowing down parallax effect - */ - private float calculateParallaxDismissingFraction(float fraction, int dockSide) { - float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; - - // Less parallax at the top, just because. - if (dockSide == WindowManager.DOCKED_TOP) { - result /= 2f; - } - return result; + // Calculate parallax and modify mRetreatingSideParallax and mAdvancingSideParallax, for use + // in adjustRootSurface(). + mParallaxSpec.getParallax(mRetreatingSideParallax, mAdvancingSideParallax, position, + snapAlgorithm, isLeftRightSplit, displayBounds, mRetreatingSurface, + mRetreatingContent, mAdvancingSurface, mAdvancingContent, mDimmingSide, + topLeftShrink); } /** Applies the calculated parallax and dimming values to task surfaces. */ @@ -250,7 +172,7 @@ class ResizingEffectPolicy { SurfaceControl advancingLeash = null; if (mParallaxType == PARALLAX_DISMISSING) { - switch (mDismissingSide) { + switch (mDimmingSide) { case DOCKED_TOP: case DOCKED_LEFT: retreatingLeash = leash1; @@ -303,14 +225,17 @@ class ResizingEffectPolicy { void adjustDimSurface(SurfaceControl.Transaction t, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { SurfaceControl targetDimLayer; - switch (mDismissingSide) { + SurfaceControl oppositeDimLayer; + switch (mDimmingSide) { case DOCKED_TOP: case DOCKED_LEFT: targetDimLayer = dimLayer1; + oppositeDimLayer = dimLayer2; break; case DOCKED_BOTTOM: case DOCKED_RIGHT: targetDimLayer = dimLayer2; + oppositeDimLayer = dimLayer1; break; case DOCKED_INVALID: default: @@ -318,7 +243,9 @@ class ResizingEffectPolicy { t.setAlpha(dimLayer2, 0).hide(dimLayer2); return; } - t.setAlpha(targetDimLayer, mDismissingDimValue) - .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); + t.setAlpha(targetDimLayer, mDimValue) + .setVisibility(targetDimLayer, mDimValue > 0.001f); + t.setAlpha(oppositeDimLayer, 0f) + .setVisibility(oppositeDimLayer, false); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index bd89f5cf45f6..708e26cc5546 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -128,6 +128,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // The touch layer is on a stage root, and is sibling with things like the app activity itself // and the app veil. We want it to be above all those. public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE; + // The dim layer is also on the stage root, and stays under the touch layer. + public static final int RESTING_DIM_LAYER = RESTING_TOUCH_LAYER - 1; // Animation specs for the swap animation private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; @@ -1201,6 +1203,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Resets layer of divider bar to make sure it is always on top. t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER); } + if (dimLayer1 != null) { + t.setLayer(dimLayer1, RESTING_DIM_LAYER); + } + if (dimLayer2 != null) { + t.setLayer(dimLayer2, RESTING_DIM_LAYER); + } copyTopLeftRefBounds(mTempRect); t.setPosition(leash1, mTempRect.left, mTempRect.top) .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java index d1d133d16ae4..ad0e7fc187e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java @@ -57,4 +57,9 @@ public class SplitState { public List<RectF> getLayout(@SplitScreenState int state) { return mSplitSpec.getSpec(state); } + + /** Returns the layout associated with the current split state. */ + public List<RectF> getCurrentLayout() { + return getLayout(mState); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt new file mode 100644 index 000000000000..bdffcf51e7d4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 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.compatui.letterbox.events + +import android.graphics.Rect +import android.view.GestureDetector +import android.view.MotionEvent +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY + +/** + * [GestureDetector.SimpleOnGestureListener] implementation which receives events from the + * Letterbox Input surface, understands the type of event and filter them based on the current + * letterbox position. + */ +class ReachabilityGestureListener( + private val taskId: Int, + private val token: WindowContainerToken?, + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) : GestureDetector.SimpleOnGestureListener() { + + // The current letterbox bounds. Double tap events are ignored when happening in these bounds. + private val activityBounds = Rect() + + override fun onDoubleTap(e: MotionEvent): Boolean { + val x = e.rawX.toInt() + val y = e.rawY.toInt() + if (!activityBounds.contains(x, y)) { + val wct = wctSupplier.get().apply { + setReachabilityOffset(token!!, taskId, x, y) + } + transitions.startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + return true + } + return false + } + + /** + * Updates the bounds for the letterboxed activity. + */ + fun updateActivityBounds(newActivityBounds: Rect) { + activityBounds.set(newActivityBounds) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt new file mode 100644 index 000000000000..5e9fe09bc840 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 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.compatui.letterbox.events + +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.transition.Transitions +import javax.inject.Inject + +/** + * A Factory for [ReachabilityGestureListener]. + */ +@WMSingleton +class ReachabilityGestureListenerFactory @Inject constructor( + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) { + /** + * @return a [ReachabilityGestureListener] implementation to listen to double tap events and + * creating the related [WindowContainerTransaction] to handle the transition. + */ + fun createReachabilityGestureListener( + taskId: Int, + token: WindowContainerToken? + ): ReachabilityGestureListener = + ReachabilityGestureListener(taskId, token, transitions, animationHandler, wctSupplier) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 35475c7ee4ce..2fd8c27d5970 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -759,7 +759,6 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - DesktopTilingDecorViewModel desktopTilingDecorViewModel, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, @@ -798,7 +797,6 @@ public abstract class WMShellModule { mainHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, bubbleController, overviewToDesktopTransitionObserver, @@ -990,7 +988,8 @@ public abstract class WMShellModule { DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, - DesktopModeCompatPolicy desktopModeCompatPolicy + DesktopModeCompatPolicy desktopModeCompatPolicy, + DesktopTilingDecorViewModel desktopTilingDecorViewModel ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -1006,7 +1005,8 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy)); + taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, + desktopTilingDecorViewModel)); } @WMSingleton @@ -1278,10 +1278,10 @@ public abstract class WMShellModule { @WMSingleton @Provides static DesktopWindowingEducationTooltipController - provideDesktopWindowingEducationTooltipController( - Context context, - AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, - DisplayController displayController) { + provideDesktopWindowingEducationTooltipController( + Context context, + AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, + DisplayController displayController) { return new DesktopWindowingEducationTooltipController( context, additionalSystemViewContainerFactory, displayController); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 7d80ee5f3bb6..f8b18f29c797 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -53,6 +53,7 @@ import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.pip2.phone.PipUiStateChangeController; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -85,11 +86,13 @@ public abstract class Pip2Module { @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, - pipUiStateChangeController, displayController, pipDesktopState); + pipUiStateChangeController, displayController, splitScreenControllerOptional, + pipDesktopState); } @WMSingleton @@ -140,9 +143,10 @@ public abstract class Pip2Module { PipBoundsState pipBoundsState, @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState, - pipDesktopState); + splitScreenControllerOptional, pipDesktopState); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 6f455df6cfec..c38558d7bde9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -26,6 +26,7 @@ import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERN import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE +import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags @@ -62,7 +63,7 @@ class DesktopDisplayEventHandler( private fun onInit() { displayController.addDisplayWindowListener(this) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { desktopTasksController.onDeskRemovedListener = this } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index c9b3ec0d3a11..1f7edb413908 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -71,9 +71,31 @@ class DesktopMixedTransitionHandler( wct: WindowContainerTransaction?, ) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct) - /** Delegates starting minimized mode transition to [FreeformTaskTransitionHandler]. */ - override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder = - freeformTaskTransitionHandler.startMinimizedModeTransition(wct) + /** + * Starts a minimize transition for [taskId], with [isLastTask] which is true if the task going + * to be minimized is the last visible task. + */ + override fun startMinimizedModeTransition( + wct: WindowContainerTransaction?, + taskId: Int, + isLastTask: Boolean, + ): IBinder { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue) { + return freeformTaskTransitionHandler.startMinimizedModeTransition( + wct, + taskId, + isLastTask, + ) + } + requireNotNull(wct) + return transitions + .startTransition(Transitions.TRANSIT_MINIMIZE, wct, /* handler= */ this) + .also { transition -> + pendingMixedTransitions.add( + PendingMixedTransition.Minimize(transition, taskId, isLastTask) + ) + } + } /** Delegates starting PiP transition to [FreeformTaskTransitionHandler]. */ override fun startPipTransition(wct: WindowContainerTransaction?): IBinder = @@ -298,7 +320,15 @@ class DesktopMixedTransitionHandler( finishTransaction: SurfaceControl.Transaction, finishCallback: TransitionFinishCallback, ): Boolean { - if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false + val shouldAnimate = + if (info.type == Transitions.TRANSIT_MINIMIZE) { + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue + } else { + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue + } + if (!shouldAnimate) { + return false + } val minimizeChange = findTaskChange(info, pending.minimizingTask) if (minimizeChange == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index 03bc42f08d59..0cc8a6a5c1a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.desktopmode -import com.android.window.flags.Flags +import android.window.DesktopExperienceFlags import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter @@ -56,7 +56,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { return controller.moveTaskToDefaultDeskAndActivate(taskId, transitionSource = UNKNOWN) } if (args.size < 3) { @@ -95,7 +95,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -116,7 +116,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -137,7 +137,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -158,7 +158,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runRemoveAllDesks(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -167,7 +167,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runMoveTaskToFront(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -188,7 +188,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runMoveTaskOutOfDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -204,12 +204,12 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.moveToFullscreen(taskId, transitionSource = UNKNOWN) + return true } private fun runCanCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -225,7 +225,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runGetActiveDeskId(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -246,7 +246,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("$prefix moveTaskToDesk <taskId> ") pw.println("$prefix Move a task with given id to desktop mode.") pw.println("$prefix moveToNextDisplay <taskId> ") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index aecbf1a23cb2..99f052832a51 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -214,6 +214,13 @@ public class DesktopModeVisualIndicator { return result; } + /** + * Returns the [DragStartState] of the visual indicator. + */ + DragStartState getDragStartState() { + return mDragStartState; + } + @VisibleForTesting Region calculateFullscreenRegion(DisplayLayout layout, int captionHeight) { final Region region = new Region(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index 4777e7f93bc9..eba1be517147 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -22,6 +22,7 @@ import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray import android.view.Display.INVALID_DISPLAY +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import androidx.core.util.forEach import androidx.core.util.valueIterator @@ -137,7 +138,7 @@ class DesktopRepository( private var desktopGestureExclusionExecutor: Executor? = null private val desktopData: DesktopData = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { MultiDesktopData() } else { SingleDesktopData() @@ -226,10 +227,19 @@ class DesktopRepository( desktopData.setActiveDesk(displayId = displayId, deskId = deskId) } + /** Sets the given desk as inactive if it was active. */ + fun setDeskInactive(deskId: Int) { + desktopData.setDeskInactive(deskId) + } + /** Returns the id of the active desk in the given display, if any. */ @VisibleForTesting fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId + /** Returns the id of the desk to which this task belongs. */ + fun getDeskIdForTask(taskId: Int): Int? = + desktopData.desksSequence().find { desk -> desk.activeTasks.contains(taskId) }?.deskId + /** * Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk. * @@ -270,20 +280,40 @@ class DesktopRepository( @VisibleForTesting fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) { val affectedDisplays = mutableSetOf<Int>() - desktopData.forAllDesks { displayId, desk -> - if (desk.deskId != excludedDeskId && desk.activeTasks.remove(taskId)) { - logD( - "Removed active task=%d displayId=%d deskId=%d", - taskId, - displayId, - desk.deskId, - ) - affectedDisplays.add(displayId) + desktopData + .desksSequence() + .filter { desk -> desk.displayId != excludedDeskId } + .forEach { desk -> + val removed = removeActiveTaskFromDesk(desk.deskId, taskId, notifyListeners = false) + if (removed) { + logD( + "Removed active task=%d displayId=%d deskId=%d", + taskId, + desk.displayId, + desk.deskId, + ) + affectedDisplays.add(desk.displayId) + } } - } affectedDisplays.forEach { displayId -> updateActiveTasksListeners(displayId) } } + private fun removeActiveTaskFromDesk( + deskId: Int, + taskId: Int, + notifyListeners: Boolean = true, + ): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + if (desk.activeTasks.remove(taskId)) { + logD("Removed active task=%d from deskId=%d", taskId, desk.deskId) + if (notifyListeners) { + updateActiveTasksListeners(desk.displayId) + } + return true + } + return false + } + /** * Adds given task to the closing task list for [displayId]'s active desk. * @@ -322,10 +352,22 @@ class DesktopRepository( fun isActiveTask(taskId: Int) = desksSequence().any { taskId in it.activeTasks } + @VisibleForTesting + fun isActiveTaskInDesk(taskId: Int, deskId: Int): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + return taskId in desk.activeTasks + } + fun isClosingTask(taskId: Int) = desksSequence().any { taskId in it.closingTasks } fun isVisibleTask(taskId: Int) = desksSequence().any { taskId in it.visibleTasks } + @VisibleForTesting + fun isVisibleTaskInDesk(taskId: Int, deskId: Int): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + return taskId in desk.visibleTasks + } + fun isMinimizedTask(taskId: Int) = desksSequence().any { taskId in it.minimizedTasks } /** @@ -415,12 +457,19 @@ class DesktopRepository( /** Removes task from visible tasks of all desks except [excludedDeskId]. */ private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) { desktopData.forAllDesks { displayId, desk -> - if (desk.deskId != excludedDeskId && desk.visibleTasks.remove(taskId)) { - notifyVisibleTaskListeners(displayId, desk.visibleTasks.size) + if (desk.deskId != excludedDeskId) { + removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId) } } } + private fun removeVisibleTaskFromDesk(deskId: Int, taskId: Int) { + val desk = desktopData.getDesk(deskId) ?: return + if (desk.visibleTasks.remove(taskId)) { + notifyVisibleTaskListeners(desk.displayId, desk.visibleTasks.size) + } + } + /** * Updates visibility of a freeform task with [taskId] on [displayId] and notifies listeners. * @@ -576,15 +625,26 @@ class DesktopRepository( /** * Set whether the given task is the full-immersive task in this display's active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with + * an explicit desk id instead of using this function and defaulting to the active one. */ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) { - val desktopData = desktopData.getActiveDesk(displayId) ?: return + val activeDesk = desktopData.getActiveDesk(displayId) ?: return + setTaskInFullImmersiveStateInDesk( + deskId = activeDesk.deskId, + taskId = taskId, + immersive = immersive, + ) + } + + /** Sets whether the given task is the full-immersive task in the given desk. */ + fun setTaskInFullImmersiveStateInDesk(deskId: Int, taskId: Int, immersive: Boolean) { + val desk = desktopData.getDesk(deskId) ?: return if (immersive) { - desktopData.fullImmersiveTaskId = taskId + desk.fullImmersiveTaskId = taskId } else { - if (desktopData.fullImmersiveTaskId == taskId) { - desktopData.fullImmersiveTaskId = null + if (desk.fullImmersiveTaskId == taskId) { + desk.fullImmersiveTaskId = null } } } @@ -674,7 +734,8 @@ class DesktopRepository( /** * Minimizes the task for [taskId] and [displayId]'s active display. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - consider forcing callers to use [minimizeTaskInDesk] with an explicit + * desk id instead of using this function and defaulting to the active one. */ fun minimizeTask(displayId: Int, taskId: Int) { if (displayId == INVALID_DISPLAY) { @@ -683,32 +744,41 @@ class DesktopRepository( getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId) return - } else { - logD("Minimize Task: display=%d, task=%d", displayId, taskId) - desktopData.getActiveDesk(displayId)?.minimizedTasks?.add(taskId) - ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) } - updateTask(displayId, taskId, isVisible = false) + val deskId = desktopData.getActiveDesk(displayId)?.deskId + if (deskId == null) { + logD("Minimize task: No active desk found for task: taskId=%d", taskId) + return + } + minimizeTaskInDesk(displayId, deskId, taskId) + } + + /** Minimizes the task in its desk. */ + @VisibleForTesting + fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) { + logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) + desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId) + ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) + updateTaskInDesk(displayId, deskId, taskId, isVisible = false) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - updatePersistentRepository(displayId) + updatePersistentRepositoryForDesk(deskId) } } /** * Unminimizes the task for [taskId] and [displayId]. * - * TODO: b/389960283 - consider adding an explicit [deskId] argument. + * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead. */ fun unminimizeTask(displayId: Int, taskId: Int) { logD("Unminimize Task: display=%d, task=%d", displayId, taskId) - var removed = false - desktopData.forAllDesks(displayId) { desk -> - if (desk.minimizedTasks.remove(taskId)) { - removed = true - } - } - if (!removed) { - logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId) + desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) } + } + + private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) { + logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId) + if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) { + logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId) } } @@ -729,7 +799,7 @@ class DesktopRepository( * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id * will be looked up from the task id. * - * TODO: b/389960283 - consider adding an explicit [deskId] argument. + * TODO: b/389960283 - consider using [removeTaskFromDesk] instead. */ fun removeTask(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d", taskId) @@ -745,24 +815,33 @@ class DesktopRepository( private fun removeTaskFromDisplay(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId) desktopData.forAllDesks(displayId) { desk -> - if (desk.freeformTasksInZOrder.remove(taskId)) { - logD( - "Remaining freeform tasks in desk: %d, tasks: %s", - desk.deskId, - desk.freeformTasksInZOrder.toDumpString(), - ) - } + removeTaskFromDesk(deskId = desk.deskId, taskId = taskId) } + } + + /** Removes the given task from the given desk. */ + fun removeTaskFromDesk(deskId: Int, taskId: Int) { + logD("removeTaskFromDesk: deskId=%d, taskId=%d", deskId, taskId) + // TODO: b/362720497 - consider not clearing bounds on any removal, such as when moving + // it between desks. It might be better to allow restoring to the previous bounds as long + // as they're valid (probably valid if in the same display). boundsBeforeMaximizeByTaskId.remove(taskId) boundsBeforeFullImmersiveByTaskId.remove(taskId) - // Remove task from unminimized task if it is minimized. - unminimizeTask(displayId, taskId) + val desk = desktopData.getDesk(deskId) ?: return + if (desk.freeformTasksInZOrder.remove(taskId)) { + logD( + "Remaining freeform tasks in desk: %d, tasks: %s", + desk.deskId, + desk.freeformTasksInZOrder.toDumpString(), + ) + } + unminimizeTaskFromDesk(deskId, taskId) // Mark task as not in immersive if it was immersive. - setTaskInFullImmersiveState(displayId = displayId, taskId = taskId, immersive = false) - removeActiveTask(taskId) - removeVisibleTask(taskId) - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - updatePersistentRepository(displayId) + setTaskInFullImmersiveStateInDesk(deskId = deskId, taskId = taskId, immersive = false) + removeActiveTaskFromDesk(deskId = deskId, taskId = taskId) + removeVisibleTaskFromDesk(deskId = deskId, taskId = taskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { + updatePersistentRepositoryForDesk(desk.deskId) } } @@ -832,24 +911,29 @@ class DesktopRepository( private fun updatePersistentRepository(displayId: Int) { val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList() mainCoroutineScope.launch { - desks.forEach { desk -> - try { - persistentRepository.addOrUpdateDesktop( - // Use display id as desk id for now since only once desk per display - // is supported. - userId = userId, - desktopId = desk.deskId, - visibleTasks = desk.visibleTasks, - minimizedTasks = desk.minimizedTasks, - freeformTasksInZOrder = desk.freeformTasksInZOrder, - ) - } catch (exception: Exception) { - logE( - "An exception occurred while updating the persistent repository \n%s", - exception.stackTrace, - ) - } - } + desks.forEach { desk -> updatePersistentRepositoryForDesk(desk) } + } + } + + private fun updatePersistentRepositoryForDesk(deskId: Int) { + val desk = desktopData.getDesk(deskId)?.deepCopy() ?: return + mainCoroutineScope.launch { updatePersistentRepositoryForDesk(desk) } + } + + private suspend fun updatePersistentRepositoryForDesk(desk: Desk) { + try { + persistentRepository.addOrUpdateDesktop( + userId = userId, + desktopId = desk.deskId, + visibleTasks = desk.visibleTasks, + minimizedTasks = desk.minimizedTasks, + freeformTasksInZOrder = desk.freeformTasksInZOrder, + ) + } catch (exception: Exception) { + logE( + "An exception occurred while updating the persistent repository \n%s", + exception.stackTrace, + ) } } @@ -866,21 +950,27 @@ class DesktopRepository( desktopData .desksSequence() .groupBy { it.displayId } - .forEach { (displayId, desks) -> + .map { (displayId, desks) -> + Triple(displayId, desktopData.getActiveDesk(displayId)?.deskId, desks) + } + .forEach { (displayId, activeDeskId, desks) -> pw.println("${prefix}Display #$displayId:") + pw.println("${innerPrefix}activeDesk=$activeDeskId") + pw.println("${innerPrefix}desks:") + val desksPrefix = "$innerPrefix " desks.forEach { desk -> - pw.println("${innerPrefix}Desk #${desk.deskId}:") - pw.print("$innerPrefix activeTasks=") + pw.println("${desksPrefix}Desk #${desk.deskId}:") + pw.print("$desksPrefix activeTasks=") pw.println(desk.activeTasks.toDumpString()) - pw.print("$innerPrefix visibleTasks=") + pw.print("$desksPrefix visibleTasks=") pw.println(desk.visibleTasks.toDumpString()) - pw.print("$innerPrefix freeformTasksInZOrder=") + pw.print("$desksPrefix freeformTasksInZOrder=") pw.println(desk.freeformTasksInZOrder.toDumpString()) - pw.print("$innerPrefix minimizedTasks=") + pw.print("$desksPrefix minimizedTasks=") pw.println(desk.minimizedTasks.toDumpString()) - pw.print("$innerPrefix fullImmersiveTaskId=") + pw.print("$desksPrefix fullImmersiveTaskId=") pw.println(desk.fullImmersiveTaskId) - pw.print("$innerPrefix topTransparentFullscreenTaskId=") + pw.print("$desksPrefix topTransparentFullscreenTaskId=") pw.println(desk.topTransparentFullscreenTaskId) } } @@ -910,6 +1000,9 @@ class DesktopRepository( /** Sets the given desk as the active desk in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) + /** Sets the desk as inactive if it was active. */ + fun setDeskInactive(deskId: Int) + /** * Returns the default desk in the given display. Useful when the system wants to activate a * desk but doesn't care about which one it activates (e.g. when putting a window into a @@ -990,6 +1083,11 @@ class DesktopRepository( // existence of visible desktop windows, among other factors. } + override fun setDeskInactive(deskId: Int) { + // No-op, in single-desk setups, which desktop is "active" is determined by the + // existence of visible desktop windows, among other factors. + } + override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId) override fun getAllActiveDesks(): Set<Desk> = @@ -1058,6 +1156,14 @@ class DesktopRepository( display.activeDeskId = desk.deskId } + override fun setDeskInactive(deskId: Int) { + desktopDisplays.forEach { id, display -> + if (display.activeDeskId == deskId) { + display.activeDeskId = null + } + } + } + override fun getDefaultDesk(displayId: Int): Desk? { val display = desktopDisplays[displayId] ?: return null return display.orderedDesks.find { it.deskId == display.activeDeskId } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt index 4d87b2189115..e831d5eecdc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt @@ -42,6 +42,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return + // TODO: b/394281403 - with multiple desks, it's possible to have a non-freeform task + // inside a desk, so this should be decoupled from windowing mode. + // Also, changes in/out of desks are handled by the [DesksTransitionObserver], which has + // more specific information about the desk involved in the transition, which might be + // more accurate than assuming it's always the default/active desk in the display, as this + // method does. // Case 1: Freeform task is changed in Desktop Mode. if (isFreeformTask(taskInfo)) { if (taskInfo.isVisible) { 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 f17b680f6fae..d767a0b5bd57 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 @@ -41,6 +41,7 @@ import android.os.Handler import android.os.IBinder import android.os.SystemProperties import android.os.UserHandle +import android.util.Slog import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.MotionEvent @@ -53,6 +54,7 @@ import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_FRONT import android.widget.Toast +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER @@ -136,7 +138,6 @@ import com.android.wm.shell.sysui.UserChangeListener import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionFinishCallback -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener @@ -144,7 +145,7 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.requestingImmersive -import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel +import com.android.wm.shell.windowdecor.tiling.SnapEventHandler import java.io.PrintWriter import java.util.Optional import java.util.concurrent.Executor @@ -152,6 +153,16 @@ import java.util.concurrent.TimeUnit import java.util.function.Consumer import kotlin.jvm.optionals.getOrNull +/** + * A callback to be invoked when a transition is started via |Transitions.startTransition| with the + * transition binder token that it produces. + * + * Useful when multiple components are appending WCT operations to a single transition that is + * started outside of their control, and each of them wants to track the transition lifecycle + * independently by cross-referencing the transition token with future ready-transitions. + */ +typealias RunOnTransitStart = (IBinder) -> Unit + /** Handles moving tasks in and out of desktop */ class DesktopTasksController( private val context: Context, @@ -184,7 +195,6 @@ class DesktopTasksController( @ShellMainThread private val handler: Handler, private val desktopModeEventLogger: DesktopModeEventLogger, private val desktopModeUiEventLogger: DesktopModeUiEventLogger, - private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, private val bubbleController: Optional<BubbleController>, private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, @@ -204,7 +214,9 @@ class DesktopTasksController( private var userId: Int private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = DesktopModeShellCommandHandler(this) + private val mOnAnimationFinishedCallback = { releaseVisualIndicator() } + private lateinit var snapEventHandler: SnapEventHandler private val dragToDesktopStateListener = object : DragToDesktopStateListener { override fun onCommitToDesktopAnimationStart() { @@ -269,7 +281,7 @@ class DesktopTasksController( RecentsTransitionStateListener.stateToString(state), ) recentsTransitionState = state - desktopTilingDecorViewModel.onOverviewAnimationStateChange( + snapEventHandler.onOverviewAnimationStateChange( RecentsTransitionStateListener.isAnimating(state) ) } @@ -300,6 +312,11 @@ class DesktopTasksController( dragToDesktopTransitionHandler.setSplitScreenController(controller) } + /** Setter to handle snap events */ + fun setSnapEventHandler(handler: SnapEventHandler) { + snapEventHandler = handler + } + /** Returns the transition type for the given remote transition. */ private fun transitionType(remoteTransition: RemoteTransition?): Int { if (remoteTransition == null) { @@ -423,7 +440,7 @@ class DesktopTasksController( /** Creates a new desk in the given display. */ fun createDesk(displayId: Int) { - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksOrganizer.createDesk(displayId) { deskId -> taskRepository.addDesk(displayId = displayId, deskId = deskId) } @@ -450,10 +467,7 @@ class DesktopTasksController( } // TODO(342378842): Instead of using default display, support multiple displays val displayId = runningTask?.displayId ?: DEFAULT_DISPLAY - val deskId = - checkNotNull(taskRepository.getDefaultDeskId(displayId)) { - "Expected a default desk to exist" - } + val deskId = getDefaultDeskId(displayId) return moveTaskToDesk( taskId = taskId, deskId = deskId, @@ -474,7 +488,7 @@ class DesktopTasksController( ): Boolean { val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) if (runningTask != null) { - moveRunningTaskToDesk( + return moveRunningTaskToDesk( task = runningTask, deskId = deskId, wct = wct, @@ -556,10 +570,10 @@ class DesktopTasksController( transitionSource: DesktopModeTransitionSource, remoteTransition: RemoteTransition? = null, callback: IMoveToDesktopCallback? = null, - ) { + ): Boolean { if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) { logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId) - return + return false } val displayId = taskRepository.getDisplayForDesk(deskId) logV( @@ -602,7 +616,7 @@ class DesktopTasksController( addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.ActiveDeskWithTask( token = transition, @@ -614,6 +628,7 @@ class DesktopTasksController( } else { taskRepository.setActiveDesk(displayId = displayId, deskId = deskId) } + return true } /** @@ -630,7 +645,7 @@ class DesktopTasksController( task: RunningTaskInfo, ): Int? { val taskIdToMinimize = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { // Activate the desk first. prepareForDeskActivation(displayId, wct) desksOrganizer.activateDesk(wct, deskId) @@ -650,7 +665,7 @@ class DesktopTasksController( // Bring other apps to front first. bringDesktopAppsToFrontBeforeShowingNewTask(displayId, wct, task.taskId) } - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { prepareMoveTaskToDesk(wct, task, deskId) } else { addMoveToDesktopChanges(wct, task) @@ -697,10 +712,7 @@ class DesktopTasksController( * [startDragToDesktop]. */ private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) { - val deskId = - checkNotNull(taskRepository.getDefaultDeskId(taskInfo.displayId)) { - "Expected a default desk to exist" - } + val deskId = getDefaultDeskId(taskInfo.displayId) ProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: finalizeDragToDesktop taskId=%d deskId=%d", @@ -709,7 +721,7 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { // |moveHomeTask| is also called in |bringDesktopAppsToFrontBeforeShowingNewTask|, so // this shouldn't be necessary at all. if (Flags.enablePerDisplayDesktopWallpaperActivity()) { @@ -741,7 +753,7 @@ class DesktopTasksController( addPendingMinimizeTransition(it, taskId, MinimizeReason.TASK_LIMIT) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.ActiveDeskWithTask( token = transition, @@ -782,22 +794,44 @@ class DesktopTasksController( wct: WindowContainerTransaction, displayId: Int, taskInfo: RunningTaskInfo, - ): ((IBinder) -> Unit)? { + ): ((IBinder) -> Unit) { val taskId = taskInfo.taskId - desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) - performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + snapEventHandler.removeTaskIfTiled(displayId, taskId) + val shouldExitDesktop = + willExitDesktop( + triggerTaskId = taskInfo.taskId, + displayId = displayId, + forceToFullscreen = false, + ) + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = shouldExitDesktop, + shouldEndUpAtHome = true, + ) + taskRepository.addClosingTask(displayId, taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) ) - return desktopImmersiveController - .exitImmersiveIfApplicable( - wct = wct, - taskInfo = taskInfo, - reason = DesktopImmersiveController.ExitReason.CLOSED, - ) - .asExit() - ?.runOnTransitionStart + + val immersiveRunnable = + desktopImmersiveController + .exitImmersiveIfApplicable( + wct = wct, + taskInfo = taskInfo, + reason = DesktopImmersiveController.ExitReason.CLOSED, + ) + .asExit() + ?.runOnTransitionStart + return { transitionToken -> + immersiveRunnable?.invoke(transitionToken) + desktopExitRunnable?.invoke(transitionToken) + } } fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { @@ -831,10 +865,20 @@ class DesktopTasksController( private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { val taskId = taskInfo.taskId + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) val displayId = taskInfo.displayId val wct = WindowContainerTransaction() - desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) - performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) + + snapEventHandler.removeTaskIfTiled(displayId, taskId) + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) + val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false) + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = willExitDesktop, + ) // Notify immersive handler as it might need to exit immersive state. val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( @@ -844,7 +888,9 @@ class DesktopTasksController( ) wct.reorder(taskInfo.token, false) - val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct) + val isLastTask = taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId) + val transition: IBinder = + freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask) desktopTasksLimiter.ifPresent { it.addPendingMinimizeChange( transition = transition, @@ -854,12 +900,13 @@ class DesktopTasksController( ) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + desktopExitRunnable?.invoke(transition) } /** Move a task with given `taskId` to fullscreen */ fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> - desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, taskId) + snapEventHandler.removeTaskIfTiled(task.displayId, taskId) moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource) } } @@ -867,7 +914,7 @@ class DesktopTasksController( /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */ fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) { getFocusedFreeformTask(displayId)?.let { - desktopTilingDecorViewModel.removeTaskIfTiled(displayId, it.taskId) + snapEventHandler.removeTaskIfTiled(displayId, it.taskId) moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource) } } @@ -901,7 +948,8 @@ class DesktopTasksController( ) { logV("moveToFullscreenWithAnimation taskId=%d", task.taskId) val wct = WindowContainerTransaction() - addMoveToFullscreenChanges(wct, task) + val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true) + val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop) // We are moving a freeform task to fullscreen, put the home task under the fullscreen task. if (!forceEnterDesktop(task.displayId)) { @@ -909,12 +957,14 @@ class DesktopTasksController( wct.reorder(task.token, /* onTop= */ true) } - exitDesktopTaskTransitionHandler.startTransition( - transitionSource, - wct, - position, - mOnAnimationFinishedCallback, - ) + val transition = + exitDesktopTaskTransitionHandler.startTransition( + transitionSource, + wct, + position, + mOnAnimationFinishedCallback, + ) + deactivationRunnable?.invoke(transition) // handles case where we are moving to full screen without closing all DW tasks. if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) { @@ -986,7 +1036,7 @@ class DesktopTasksController( logV("moveTaskToFront taskId=%s", taskInfo.taskId) // If a task is tiled, another task should be brought to foreground with it so let // tiling controller handle the request. - if (desktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo)) { + if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) { return } val wct = WindowContainerTransaction() @@ -1173,6 +1223,8 @@ class DesktopTasksController( wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) } + // TODO: b/394268248 - desk needs to be deactivated when moving the last task and going + // home. if (Flags.enablePerDisplayDesktopWallpaperActivity()) { performDesktopExitCleanupIfNeeded( task.taskId, @@ -1228,7 +1280,7 @@ class DesktopTasksController( } else { // Save current bounds so that task can be restored back to original bounds if necessary // and toggle to the stable bounds. - desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) + snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds) destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo)) } @@ -1354,7 +1406,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopWindowDecoration: DesktopModeWindowDecoration, ) { desktopModeEventLogger.logTaskResizingStarted( resizeTrigger, @@ -1376,13 +1427,7 @@ class DesktopTasksController( ) if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) { - val isTiled = - desktopTilingDecorViewModel.snapToHalfScreen( - taskInfo, - desktopWindowDecoration, - position, - currentDragBounds, - ) + val isTiled = snapEventHandler.snapToHalfScreen(taskInfo, currentDragBounds, position) if (isTiled) { taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true) } @@ -1419,7 +1464,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (!isSnapResizingAllowed(taskInfo)) { Toast.makeText( @@ -1438,7 +1482,6 @@ class DesktopTasksController( position, resizeTrigger, inputMethod, - desktopModeWindowDecoration, ) } @@ -1450,7 +1493,6 @@ class DesktopTasksController( currentDragBounds: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { releaseVisualIndicator() if (!isSnapResizingAllowed(taskInfo)) { @@ -1498,7 +1540,6 @@ class DesktopTasksController( position, resizeTrigger, DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent), - desktopModeWindowDecoration, ) } } @@ -1547,7 +1588,7 @@ class DesktopTasksController( private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) { // Move home to front, ensures that we go back home when all desktop windows are closed val useParamDisplayId = - Flags.enableMultipleDesktopsBackend() || + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue || Flags.enablePerDisplayDesktopWallpaperActivity() moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct) // Currently, we only handle the desktop on the default display really. @@ -1730,33 +1771,59 @@ class DesktopTasksController( } } - /** - * Remove wallpaper activity if task provided is last task and wallpaper activity token is not - * null - */ - private fun performDesktopExitCleanupIfNeeded( - taskId: Int, + private fun willExitDesktop( + triggerTaskId: Int, displayId: Int, - wct: WindowContainerTransaction, forceToFullscreen: Boolean, - shouldEndUpAtHome: Boolean = true, - ) { - taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen) + ): Boolean { if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) { - return + if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) { + return false } } else if ( Flags.enableDesktopWindowingPip() && taskRepository.isMinimizedPipPresentInDisplay(displayId) && !forceToFullscreen ) { - return + return false } else { - if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) { - return + if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId)) { + return false } } + return true + } + + private fun performDesktopExitCleanupIfNeeded( + taskId: Int, + displayId: Int, + wct: WindowContainerTransaction, + forceToFullscreen: Boolean, + shouldEndUpAtHome: Boolean = true, + ): RunOnTransitStart? { + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = !forceToFullscreen) + if (!willExitDesktop(taskId, displayId, forceToFullscreen)) { + return null + } + // TODO: b/394268248 - update remaining callers to pass in a |deskId| and apply the + // |RunOnTransitStart| when the transition is started. + return performDesktopExitCleanUp( + wct = wct, + deskId = null, + displayId = displayId, + willExitDesktop = true, + shouldEndUpAtHome = shouldEndUpAtHome, + ) + } + + private fun performDesktopExitCleanUp( + wct: WindowContainerTransaction, + deskId: Int?, + displayId: Int, + willExitDesktop: Boolean, + shouldEndUpAtHome: Boolean = true, + ): RunOnTransitStart? { + if (!willExitDesktop) return null desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( FULLSCREEN_ANIMATION_DURATION ) @@ -1766,6 +1833,7 @@ class DesktopTasksController( // intent. addLaunchHomePendingIntent(wct, displayId) } + return prepareDeskDeactivationIfNeeded(wct, deskId) } fun releaseVisualIndicator() { @@ -1976,8 +2044,10 @@ class DesktopTasksController( unminimizeReason = UnminimizeReason.APP_HANDLE_MENU_BUTTON, ) } else { - moveBackgroundTaskToDesktop( + val deskId = getDefaultDeskId(callingTask.displayId) + moveTaskToDesk( requestedTaskId, + deskId, WindowContainerTransaction(), DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON, ) @@ -2095,7 +2165,16 @@ class DesktopTasksController( ): WindowContainerTransaction? { logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch") val wct = WindowContainerTransaction() - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) wct.reorder(task.token, true) return wct } @@ -2119,7 +2198,16 @@ class DesktopTasksController( // launched. We should make this task go to fullscreen instead of freeform. Note // that this means any re-launch of a freeform window outside of desktop will be in // fullscreen as long as default-desktop flag is disabled. - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) return wct } bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) @@ -2169,7 +2257,7 @@ class DesktopTasksController( return wct } if (!wct.isEmpty) { - desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId) + snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) return wct } return null @@ -2215,7 +2303,16 @@ class DesktopTasksController( // changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED // set when needed can interfere with future split / multi-instance transitions. return WindowContainerTransaction().also { wct -> - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) } } return null @@ -2243,10 +2340,25 @@ class DesktopTasksController( } // Already fullscreen, no-op. if (task.isFullscreen) return null - return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) } + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) + } } - /** Handle task closing by removing wallpaper activity if it's the last active task */ + /** + * Handle task closing by removing wallpaper activity if it's the last active task. + * + * TODO: b/394268248 - desk needs to be deactivated. + */ private fun handleTaskClosing( task: RunningTaskInfo, transition: IBinder, @@ -2265,7 +2377,7 @@ class DesktopTasksController( if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { taskRepository.addClosingTask(task.displayId, task.taskId) - desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId) + snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) } taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( @@ -2313,7 +2425,7 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, deskId: Int, ) { - if (!Flags.enableMultipleDesktopsBackend()) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return val displayId = taskRepository.getDisplayForDesk(deskId) val displayLayout = displayController.getDisplayLayout(displayId) ?: return val initialBounds = getInitialBounds(displayLayout, taskInfo, displayId) @@ -2397,10 +2509,15 @@ class DesktopTasksController( return bounds } + /** + * Applies the changes needed to enter fullscreen and returns the id of the desk that needs to + * be deactivated. + */ private fun addMoveToFullscreenChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, - ) { + willExitDesktop: Boolean, + ): RunOnTransitStart? { val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode val targetWindowingMode = @@ -2415,12 +2532,16 @@ class DesktopTasksController( if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } - - performDesktopExitCleanupIfNeeded( - taskInfo.taskId, - taskInfo.displayId, - wct, - forceToFullscreen = true, + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true) + } + taskRepository.setPipShouldKeepDesktopActive(taskInfo.displayId, keepActive = false) + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + return performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = taskInfo.displayId, + willExitDesktop = willExitDesktop, shouldEndUpAtHome = false, ) } @@ -2445,6 +2566,8 @@ class DesktopTasksController( /** * Adds split screen changes to a transaction. Note that bounds are not reset here due to * animation; see {@link onDesktopSplitSelectAnimComplete} + * + * TODO: b/394268248 - desk needs to be deactivated. */ private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { // This windowing mode is to get the transition animation started; once we complete @@ -2534,10 +2657,7 @@ class DesktopTasksController( displayId: Int, remoteTransition: RemoteTransition? = null, ) { - val deskId = - checkNotNull(taskRepository.getDefaultDeskId(displayId)) { - "Expected a default desk to exist" - } + val deskId = getDefaultDeskId(displayId) activateDesk(deskId, remoteTransition) } @@ -2545,7 +2665,7 @@ class DesktopTasksController( fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) { val displayId = taskRepository.getDisplayForDesk(deskId) val wct = WindowContainerTransaction() - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { prepareForDeskActivation(displayId, wct) desksOrganizer.activateDesk(wct, deskId) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { @@ -2566,7 +2686,7 @@ class DesktopTasksController( val transition = transitions.startTransition(transitionType, wct, handler) handler?.setTransition(transition) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.ActivateDesk( token = transition, @@ -2581,16 +2701,35 @@ class DesktopTasksController( ) } + /** + * TODO: b/393978539 - Deactivation should not happen in desktop-first devices when going home. + */ + private fun prepareDeskDeactivationIfNeeded( + wct: WindowContainerTransaction, + deskId: Int?, + ): RunOnTransitStart? { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return null + if (deskId == null) return null + desksOrganizer.deactivateDesk(wct, deskId) + return { transition -> + desksTransitionObserver.addPendingTransition( + DeskTransition.DeactivateDesk(token = transition, deskId = deskId) + ) + } + } + /** Removes the default desk in the given display. */ @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()")) fun removeDefaultDeskInDisplay(displayId: Int) { - val deskId = - checkNotNull(taskRepository.getDefaultDeskId(displayId)) { - "Expected a default desk to exist" - } + val deskId = getDefaultDeskId(displayId) removeDesk(displayId = displayId, deskId = deskId) } + private fun getDefaultDeskId(displayId: Int) = + checkNotNull(taskRepository.getDefaultDeskId(displayId)) { + "Expected a default desk to exist in display: $displayId" + } + /** Removes the given desk. */ fun removeDesk(deskId: Int) { val displayId = taskRepository.getDisplayForDesk(deskId) @@ -2602,7 +2741,7 @@ class DesktopTasksController( logV("removeDesk deskId=%d from displayId=%d", deskId, displayId) val tasksToRemove = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { taskRepository.getActiveTaskIdsInDesk(deskId) } else { // TODO: 362720497 - make sure minimized windows are also removed in WM @@ -2611,7 +2750,7 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { tasksToRemove.forEach { val task = shellTaskOrganizer.getRunningTaskInfo(it) if (task != null) { @@ -2624,9 +2763,9 @@ class DesktopTasksController( // TODO: 362720497 - double check background tasks are also removed. desksOrganizer.removeDesk(wct, deskId) } - if (!Flags.enableMultipleDesktopsBackend() && wct.isEmpty) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksTransitionObserver.addPendingTransition( DeskTransition.RemoveDesk( token = transition, @@ -2730,7 +2869,7 @@ class DesktopTasksController( taskBounds: Rect, ) { if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return - desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) + snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) updateVisualIndicator( taskInfo, taskSurface, @@ -2747,6 +2886,12 @@ class DesktopTasksController( taskTop: Float, dragStartState: DragStartState, ): DesktopModeVisualIndicator.IndicatorType { + // If the visual indicator has the wrong start state, it was never cleared from a previous + // drag event and needs to be cleared + if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) { + Slog.e(TAG, "Visual indicator from previous motion event was never released") + releaseVisualIndicator() + } // If the visual indicator does not exist, create it. val indicator = visualIndicator @@ -2790,7 +2935,6 @@ class DesktopTasksController( validDragArea: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return @@ -2829,7 +2973,6 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { @@ -2844,7 +2987,6 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } IndicatorType.NO_INDICATOR, @@ -3130,7 +3272,7 @@ class DesktopTasksController( logV("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId) userId = newUserId taskRepository = userRepositories.getProfile(userId) - desktopTilingDecorViewModel.onUserChange() + snapEventHandler.onUserChange() } /** Called when a task's info changes. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index fc29498291da..0929ae15e668 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -251,14 +251,15 @@ sealed class DragToDesktopTransitionHandler( (cancelState == CancelState.CANCEL_BUBBLE_LEFT || cancelState == CancelState.CANCEL_BUBBLE_RIGHT) ) { - if (!bubbleController.isPresent) { + if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { + // TODO(b/388853233): add support for dragging split task to bubble startCancelAnimation() } else { // Animation is handled by BubbleController val wct = WindowContainerTransaction() restoreWindowOrder(wct, state) - // TODO(b/388851898): pass along information about left or right side - requestBubbleFromScaledTask(wct) + val onLeft = cancelState == CancelState.CANCEL_BUBBLE_LEFT + requestBubbleFromScaledTask(wct, onLeft) } } else { // There's no dragged task, this can happen when the "cancel" happened too quickly @@ -318,23 +319,27 @@ sealed class DragToDesktopTransitionHandler( splitScreenController.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds) } - private fun requestBubbleFromScaledTask(wct: WindowContainerTransaction) { + private fun requestBubbleFromScaledTask(wct: WindowContainerTransaction, onLeft: Boolean) { // TODO(b/391928049): update density once we can drag from desktop to bubble val state = requireTransitionState() val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") val taskBounds = getAnimatedTaskBounds() state.dragAnimator.cancelAnimator() - requestBubble(wct, taskInfo, taskBounds) + requestBubble(wct, taskInfo, onLeft, taskBounds) } private fun requestBubble( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, + onLeft: Boolean, taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds), ) { val controller = bubbleController.orElseThrow { IllegalStateException("BubbleController not set") } - controller.expandStackAndSelectBubble(taskInfo, BubbleTransitions.DragData(taskBounds, wct)) + controller.expandStackAndSelectBubble( + taskInfo, + BubbleTransitions.DragData(taskBounds, wct, onLeft), + ) } override fun startAnimation( @@ -493,12 +498,17 @@ sealed class DragToDesktopTransitionHandler( state.cancelState == CancelState.CANCEL_BUBBLE_LEFT || state.cancelState == CancelState.CANCEL_BUBBLE_RIGHT ) { + if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { + // TODO(b/388853233): add support for dragging split task to bubble + startCancelDragToDesktopTransition() + return true + } val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.") val wct = WindowContainerTransaction() restoreWindowOrder(wct) - // TODO(b/388851898): pass along information about left or right side - requestBubble(wct, taskInfo) + val onLeft = state.cancelState == CancelState.CANCEL_BUBBLE_LEFT + requestBubble(wct, taskInfo, onLeft) } return true } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index 5ae1fca73d4e..95cc1e68ac11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -106,7 +106,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH * @param position Position of the task when transition is started * @param onAnimationEndCallback to be called after animation */ - public void startTransition(@NonNull DesktopModeTransitionSource transitionSource, + public IBinder startTransition(@NonNull DesktopModeTransitionSource transitionSource, @NonNull WindowContainerTransaction wct, Point position, Function0<Unit> onAnimationEndCallback) { mPosition = position; @@ -114,6 +114,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource), wct, this); mPendingTransitionTokens.add(token); + return token; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt index 2a8a3475c2a5..b5490cb4b595 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt @@ -20,7 +20,7 @@ import android.util.SparseArray import android.util.SparseBooleanArray import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerToken -import androidx.core.util.forEach +import androidx.core.util.keyIterator import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -45,11 +45,13 @@ class DesktopWallpaperActivityTokenProvider { } fun removeToken(token: WindowContainerToken) { - wallpaperActivityTokenByDisplayId.forEach { displayId, value -> - if (value == token) { - logV("Remove desktop wallpaper activity token for display %s", displayId) - wallpaperActivityTokenByDisplayId.delete(displayId) + val displayId = + wallpaperActivityTokenByDisplayId.keyIterator().asSequence().find { + wallpaperActivityTokenByDisplayId[it] == token } + if (displayId != null) { + logV("Remove desktop wallpaper activity token for display %s", displayId) + wallpaperActivityTokenByDisplayId.delete(displayId) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt index 8c4fd9db050f..9dec96933ee5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt @@ -42,4 +42,7 @@ sealed class DeskTransition { val deskId: Int, val enterTaskId: Int, ) : DeskTransition() + + /** A transition to deactivate a desk. */ + data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 547890a6200a..0f2f3711a9a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -27,6 +27,9 @@ interface DesksOrganizer { /** Activates the given desk, making it visible in its display. */ fun activateDesk(wct: WindowContainerTransaction, deskId: Int) + /** Deactivates the given desk, removing it as the default launch container for new tasks. */ + fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) + /** Removes the given desk and its desktop windows. */ fun removeDesk(wct: WindowContainerTransaction, deskId: Int) @@ -37,6 +40,9 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Whether the change is for the given desk id. */ + fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean + /** * Returns the desk id in which the task in the given change is located at the end of a * transition, if any. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt index 6d88c3310a63..e57b56378fb3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt @@ -17,9 +17,11 @@ package com.android.wm.shell.desktopmode.multidesks import android.os.IBinder import android.view.WindowManager.TRANSIT_CLOSE +import android.window.DesktopExperienceFlags import android.window.TransitionInfo -import com.android.window.flags.Flags +import com.android.internal.protolog.ProtoLog import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE /** * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It @@ -33,7 +35,7 @@ class DesksTransitionObserver( /** Adds a pending desk transition to be tracked. */ fun addPendingTransition(transition: DeskTransition) { - if (!Flags.enableMultipleDesktopsBackend()) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return deskTransitions[transition.token] = transition } @@ -42,8 +44,9 @@ class DesksTransitionObserver( * observer. */ fun onTransitionReady(transition: IBinder, info: TransitionInfo) { - if (!Flags.enableMultipleDesktopsBackend()) return + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return val deskTransition = deskTransitions.remove(transition) ?: return + logD("Desk transition ready: %s", deskTransition) val desktopRepository = desktopUserRepositories.current when (deskTransition) { is DeskTransition.RemoveDesk -> { @@ -88,6 +91,42 @@ class DesksTransitionObserver( ) } } + is DeskTransition.DeactivateDesk -> { + var visibleDeactivation = false + for (change in info.changes) { + val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId) + if (isDeskChange) { + visibleDeactivation = true + continue + } + val taskId = change.taskInfo?.taskId ?: continue + val removedFromDesk = + desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId && + desksOrganizer.getDeskAtEnd(change) == null + if (removedFromDesk) { + desktopRepository.removeTaskFromDesk( + deskId = deskTransition.deskId, + taskId = taskId, + ) + } + } + // Always deactivate even if there's no change that confirms the desk was + // deactivated. Some interactions, such as the desk deactivating because it's + // occluded by a fullscreen task result in a transition change, but others, such + // as transitioning from an empty desk to home may not. + if (!visibleDeactivation) { + logD("Deactivating desk without transition change") + } + desktopRepository.setDeskInactive(deskId = deskTransition.deskId) + } } } + + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private companion object { + private const val TAG = "DesksTransitionObserver" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index 5cda76e2f3e0..339932cabd2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -23,12 +23,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.util.SparseArray import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.DesktopExperienceFlags import android.window.TransitionInfo import android.window.WindowContainerTransaction import androidx.core.util.forEach import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -47,7 +47,7 @@ class RootTaskDesksOrganizer( @VisibleForTesting val roots = SparseArray<DeskRoot>() init { - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { shellInit.addInitCallback( { shellCommandHandler.addDumpCallback(this::dump, this) }, this, @@ -83,6 +83,16 @@ class RootTaskDesksOrganizer( ) } + override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) { + logV("deactivateDesk %d", deskId) + val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ null, + /* activityTypes= */ null, + ) + } + override fun moveTaskToDesk( wct: WindowContainerTransaction, deskId: Int, @@ -93,6 +103,9 @@ class RootTaskDesksOrganizer( wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) } + override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean = + roots.contains(deskId) && change.taskInfo?.taskId == deskId + override fun getDeskAtEnd(change: TransitionInfo.Change): Int? = change.taskInfo?.parentTaskId?.takeIf { it in roots } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt index 5a89451ffdbc..0507e59c06e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.desktopmode.persistence import android.content.Context +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags -import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.shared.annotations.ShellMainThread @@ -58,7 +58,7 @@ class DesktopRepositoryInitializerImpl( repository.addDesk( displayId = persistentDesktop.displayId, deskId = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { persistentDesktop.desktopId } else { // When disabled, desk ids are always the display id. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java index 31715f0444a9..b60fb5e7bfdd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -93,7 +93,8 @@ public class FreeformTaskTransitionHandler } @Override - public IBinder startMinimizedModeTransition(WindowContainerTransaction wct) { + public IBinder startMinimizedModeTransition( + WindowContainerTransaction wct, int taskId, boolean isLastTask) { final int type = Transitions.TRANSIT_MINIMIZE; final IBinder token = mTransitions.startTransition(type, wct, this); mPendingTransitionTokens.add(token); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java index a874a5be426d..822934c1e646 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java @@ -38,10 +38,13 @@ public interface FreeformTaskTransitionStarter { * Starts window minimization transition * * @param wct the {@link WindowContainerTransaction} that changes the windowing mode + * @param taskId the task id of the task being minimized + * @param isLastTask true if the task being minimized is the last visible task * * @return the started transition */ - IBinder startMinimizedModeTransition(WindowContainerTransaction wct); + IBinder startMinimizedModeTransition( + WindowContainerTransaction wct, int taskId, boolean isLastTask); /** * Starts close window transition @@ -60,4 +63,4 @@ public interface FreeformTaskTransitionStarter { * @return the started transition */ IBinder startPipTransition(WindowContainerTransaction wct); -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index c0a0f469add4..d666126b91ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -22,7 +22,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.service.dreams.Flags.dismissDreamOnKeyguardDismiss; import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS; -import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; @@ -201,8 +200,7 @@ public class KeyguardTransitionHandler transition, info, startTransaction, finishTransaction, finishCallback); } - if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0 - || (info.getFlags() & TRANSIT_FLAG_AOD_APPEARING) != 0) { + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) { return startAnimation(mAppearTransition, "appearing", transition, info, startTransaction, finishTransaction, finishCallback); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index da3181096d98..cef18f55b86d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -145,7 +145,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Called when the Shell wants to start an exit-via-expand from Pip transition/animation. */ - public void startExpandTransition(WindowContainerTransaction out) { + public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) { // Default implementation does nothing. } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java index 71697596afd3..a837e7d308eb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java @@ -296,6 +296,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen return; } + mMagneticTarget.updateLocationOnScreen(); createOrUpdateDismissTarget(); if (mTargetViewContainer.getVisibility() != View.VISIBLE) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index e17587ff18bc..df7a25af8376 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -35,6 +35,10 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.split.SplitScreenConstants; +import com.android.wm.shell.splitscreen.SplitScreenController; + +import java.util.Optional; /** * Scheduler for Shell initiated PiP transitions and animations. @@ -47,6 +51,7 @@ public class PipScheduler { private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private final PipDesktopState mPipDesktopState; + private final Optional<SplitScreenController> mSplitScreenControllerOptional; private PipTransitionController mPipTransitionController; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; @@ -59,12 +64,14 @@ public class PipScheduler { PipBoundsState pipBoundsState, ShellExecutor mainExecutor, PipTransitionState pipTransitionState, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; mPipDesktopState = pipDesktopState; + mSplitScreenControllerOptional = splitScreenControllerOptional; mSurfaceControlTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); @@ -96,10 +103,23 @@ public class PipScheduler { public void scheduleExitPipViaExpand() { mMainExecutor.execute(() -> { if (!mPipTransitionState.isInPip()) return; - WindowContainerTransaction wct = getExitPipViaExpandTransaction(); - if (wct != null) { - mPipTransitionController.startExpandTransition(wct); - } + + final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction(); + if (expandWct == null) return; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSplitScreenControllerOptional.ifPresent(splitScreenController -> { + int lastParentTaskId = mPipTransitionState.getPipTaskInfo() + .lastParentTaskIdBeforePip; + if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) { + splitScreenController.prepareEnterSplitScreen(wct, + null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED); + } + }); + + boolean toSplit = !wct.isEmpty(); + wct.merge(expandWct, true /* transfer */); + mPipTransitionController.startExpandTransition(wct, toSplit); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index a57b4b948b42..035c93db7ee4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -16,7 +16,6 @@ package com.android.wm.shell.pip2.phone; -import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Surface.ROTATION_0; @@ -29,7 +28,13 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipChange; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP; import static com.android.wm.shell.transition.Transitions.transitTypeToString; @@ -45,7 +50,6 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; -import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -70,11 +74,14 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; -import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.transition.PipExpandHandler; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import java.util.Optional; + /** * Implementation of transitions for PiP on phone. */ @@ -130,6 +137,7 @@ public class PipTransition extends PipTransitionController implements // // Internal state and relevant cached info // + private final PipExpandHandler mExpandHandler; private Transitions.TransitionFinishCallback mFinishCallback; @@ -151,6 +159,7 @@ public class PipTransition extends PipTransitionController implements PipDisplayLayoutState pipDisplayLayoutState, PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, + Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -165,6 +174,9 @@ public class PipTransition extends PipTransitionController implements mDisplayController = displayController; mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext); mPipDesktopState = pipDesktopState; + + mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm, + pipTransitionState, pipDisplayLayoutState, splitScreenControllerOptional); } @Override @@ -184,10 +196,11 @@ public class PipTransition extends PipTransitionController implements // @Override - public void startExpandTransition(WindowContainerTransaction out) { + public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) { if (out == null) return; mPipTransitionState.setState(PipTransitionState.EXITING_PIP); - mExitViaExpandTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this); + mExitViaExpandTransition = mTransitions.startTransition(toSplit ? TRANSIT_EXIT_PIP_TO_SPLIT + : TRANSIT_EXIT_PIP, out, this); } @Override @@ -239,10 +252,11 @@ public class PipTransition extends PipTransitionController implements @NonNull SurfaceControl.Transaction finishT, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - // Just jump-cut the current animation if any, but do not merge. if (info.getType() == TRANSIT_EXIT_PIP) { end(); } + mExpandHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); } @Override @@ -290,7 +304,8 @@ public class PipTransition extends PipTransitionController implements finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; - return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); + return mExpandHandler.startAnimation(transition, info, startTransaction, + finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); @@ -300,6 +315,9 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.setState(PipTransitionState.EXITING_PIP); return startRemoveAnimation(info, startTransaction, finishTransaction, finishCallback); } + // For any unhandled transition, make sure the PiP surface is properly updated, + // i.e. corner and shadow radius. + syncPipSurfaceState(info, startTransaction, finishTransaction); return false; } @@ -433,7 +451,7 @@ public class PipTransition extends PipTransitionController implements (destinationBounds.height() - overlaySize) / 2f); } - final int delta = getFixedRotationDelta(info, pipChange); + final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); if (delta != ROTATION_0) { // Update transition target changes in place to prepare for fixed rotation. handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); @@ -493,7 +511,7 @@ public class PipTransition extends PipTransitionController implements final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange, pipActivityChange); - final int delta = getFixedRotationDelta(info, pipChange); + final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); if (delta != ROTATION_0) { // Update transition target changes in place to prepare for fixed rotation. handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); @@ -582,27 +600,6 @@ public class PipTransition extends PipTransitionController implements endBounds.top + activityEndOffset.y); } - private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { - final Rect endBounds = outPipTaskChange.getEndAbsBounds(); - final int width = endBounds.width(); - final int height = endBounds.height(); - final int left = endBounds.left; - final int top = endBounds.top; - int newTop, newLeft; - - if (delta == Surface.ROTATION_90) { - newLeft = top; - newTop = -(left + width); - } else { - newLeft = -(height + top); - newTop = left; - } - // Modify the endBounds, rotating and placing them potentially off-screen, so that - // as we translate and rotate around the origin, we place them right into the target. - endBounds.set(newLeft, newTop, newLeft + height, newTop + width); - } - - private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -630,83 +627,6 @@ public class PipTransition extends PipTransitionController implements return true; } - private boolean startExpandAnimation(@NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); - - TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); - if (pipChange == null) { - // pipChange is null, check to see if we've reparented the PIP activity for - // the multi activity case. If so we should use the activity leash instead - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() == null - && change.getLastParent() != null - && change.getLastParent().equals(pipToken)) { - pipChange = change; - break; - } - } - - // failsafe - if (pipChange == null) { - return false; - } - } - mFinishCallback = finishCallback; - - // The parent change if we were in a multi-activity PiP; null if single activity PiP. - final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null - ? getChangeByToken(info, pipChange.getParent()) : null; - if (parentBeforePip != null) { - // For multi activity, we need to manually set the leash layer - startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1); - } - - final Rect startBounds = pipChange.getStartAbsBounds(); - final Rect endBounds = pipChange.getEndAbsBounds(); - final SurfaceControl pipLeash = getLeash(pipChange); - - PictureInPictureParams params = null; - if (pipChange.getTaskInfo() != null) { - // single activity - params = getPipParams(pipChange); - } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) { - // multi activity - params = getPipParams(parentBeforePip); - } - final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, - startBounds); - - // We define delta = startRotation - endRotation, so we need to flip the sign. - final int delta = -getFixedRotationDelta(info, pipChange); - if (delta != ROTATION_0) { - // Update PiP target change in place to prepare for fixed rotation; - handleExpandFixedRotation(pipChange, delta); - } - - PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash, - startTransaction, finishTransaction, endBounds, startBounds, endBounds, - sourceRectHint, delta); - animator.setAnimationEndCallback(() -> { - if (parentBeforePip != null) { - // TODO b/377362511: Animate local leash instead to also handle letterbox case. - // For multi-activity, set the crop to be null - finishTransaction.setCrop(pipLeash, null); - } - finishTransition(); - }); - cacheAndStartTransitionAnimator(animator); - - // Save the PiP bounds in case, we re-enter the PiP with the same component. - float snapFraction = mPipBoundsAlgorithm.getSnapFraction( - mPipBoundsState.getBounds()); - mPipBoundsState.saveReentryState(snapFraction); - - return true; - } - private boolean startRemoveAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -740,29 +660,6 @@ public class PipTransition extends PipTransitionController implements // Various helpers to resolve transition requests and infos // - @Nullable - private TransitionInfo.Change getPipChange(TransitionInfo info) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() != null - && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { - return change; - } - } - return null; - } - - @Nullable - private TransitionInfo.Change getChangeByToken(TransitionInfo info, - WindowContainerToken token) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() != null - && change.getTaskInfo().getToken().equals(token)) { - return change; - } - } - return null; - } - @NonNull private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change pipTaskChange, @@ -786,8 +683,8 @@ public class PipTransition extends PipTransitionController implements Rect cutoutInsets = parentBeforePip != null ? parentBeforePip.getTaskInfo().displayCutoutInsets : pipTaskChange.getTaskInfo().displayCutoutInsets; - if (cutoutInsets != null - && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) { + if (cutoutInsets != null && getFixedRotationDelta(info, pipTaskChange, + mPipDisplayLayoutState) == ROTATION_90) { adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top); } if (mPipDesktopState.isDesktopWindowingPipEnabled()) { @@ -804,25 +701,6 @@ public class PipTransition extends PipTransitionController implements return adjustedSourceRectHint; } - @Surface.Rotation - private int getFixedRotationDelta(@NonNull TransitionInfo info, - @NonNull TransitionInfo.Change pipChange) { - TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); - int startRotation = pipChange.getStartRotation(); - if (pipChange.getEndRotation() != ROTATION_UNDEFINED - && startRotation != pipChange.getEndRotation()) { - // If PiP change was collected along with the display change and the orientation change - // happened in sync with the PiP change, then do not treat this as fixed-rotation case. - return ROTATION_0; - } - - int endRotation = fixedRotationChange != null - ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation(); - int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : startRotation - endRotation; - return delta; - } - private void prepareOtherTargetTransforms(TransitionInfo info, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction) { @@ -850,7 +728,8 @@ public class PipTransition extends PipTransitionController implements // If PiP is enabled on Connected Displays, update PipDisplayLayoutState to have the correct // display info that PiP is entering in. - if (mPipDesktopState.isConnectedDisplaysPipEnabled()) { + if (mPipDesktopState.isConnectedDisplaysPipEnabled() + && pipTask.displayId != mPipDisplayLayoutState.getDisplayId()) { final DisplayLayout displayLayout = mDisplayController.getDisplayLayout( pipTask.displayId); if (displayLayout != null) { @@ -1009,20 +888,6 @@ public class PipTransition extends PipTransitionController implements mTransitionAnimator.start(); } - @NonNull - private static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) { - return pipChange.getTaskInfo().pictureInPictureParams != null - ? pipChange.getTaskInfo().pictureInPictureParams - : new PictureInPictureParams.Builder().build(); - } - - @NonNull - private static SurfaceControl getLeash(TransitionInfo.Change change) { - SurfaceControl leash = change.getLeash(); - Preconditions.checkNotNull(leash, "Leash is null for change=" + change); - return leash; - } - // // Miscellaneous callbacks and listeners // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 8805cbb0dfbd..18c9a705dcf7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -314,7 +314,8 @@ public class PipTransitionState { mSwipePipToHomeAppBounds.setEmpty(); } - @Nullable WindowContainerToken getPipTaskToken() { + @Nullable + public WindowContainerToken getPipTaskToken() { return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java new file mode 100644 index 000000000000..db4942b2fb95 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2025 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.pip2.phone.transition; + +import static android.view.Surface.ROTATION_0; + +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; + +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PictureInPictureParams; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipTransitionState; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +public class PipExpandHandler implements Transitions.TransitionHandler { + private final Context mContext; + private final PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipTransitionState mPipTransitionState; + private final PipDisplayLayoutState mPipDisplayLayoutState; + private final Optional<SplitScreenController> mSplitScreenControllerOptional; + + @Nullable + private Transitions.TransitionFinishCallback mFinishCallback; + @Nullable + private ValueAnimator mTransitionAnimator; + + private PipExpandAnimatorSupplier mPipExpandAnimatorSupplier; + + public PipExpandHandler(Context context, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipTransitionState pipTransitionState, + PipDisplayLayoutState pipDisplayLayoutState, + Optional<SplitScreenController> splitScreenControllerOptional) { + mContext = context; + mPipBoundsState = pipBoundsState; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipTransitionState = pipTransitionState; + mPipDisplayLayoutState = pipDisplayLayoutState; + mSplitScreenControllerOptional = splitScreenControllerOptional; + + mPipExpandAnimatorSupplier = PipExpandAnimator::new; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + // All Exit-via-Expand from PiP transitions are Shell initiated. + return null; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + switch (info.getType()) { + case TRANSIT_EXIT_PIP: + return startExpandAnimation(info, startTransaction, finishTransaction, + finishCallback); + case TRANSIT_EXIT_PIP_TO_SPLIT: + return startExpandToSplitAnimation(info, startTransaction, finishTransaction, + finishCallback); + } + return false; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + end(); + } + + /** + * Ends the animation if such is running in the context of expanding out of PiP. + */ + public void end() { + if (mTransitionAnimator != null && mTransitionAnimator.isRunning()) { + mTransitionAnimator.end(); + mTransitionAnimator = null; + } + } + + private boolean startExpandAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); + + TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); + if (pipChange == null) { + // pipChange is null, check to see if we've reparented the PIP activity for + // the multi activity case. If so we should use the activity leash instead + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + + // failsafe + if (pipChange == null) { + return false; + } + } + mFinishCallback = finishCallback; + + // The parent change if we were in a multi-activity PiP; null if single activity PiP. + final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null + ? getChangeByToken(info, pipChange.getParent()) : null; + if (parentBeforePip != null) { + // For multi activity, we need to manually set the leash layer + startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1); + } + + final Rect startBounds = pipChange.getStartAbsBounds(); + final Rect endBounds = pipChange.getEndAbsBounds(); + final SurfaceControl pipLeash = getLeash(pipChange); + + PictureInPictureParams params = null; + if (pipChange.getTaskInfo() != null) { + // single activity + params = getPipParams(pipChange); + } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) { + // multi activity + params = getPipParams(parentBeforePip); + } + final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, + startBounds); + + // We define delta = startRotation - endRotation, so we need to flip the sign. + final int delta = -getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); + if (delta != ROTATION_0) { + // Update PiP target change in place to prepare for fixed rotation; + handleExpandFixedRotation(pipChange, delta); + } + + PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash, + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + sourceRectHint, delta); + animator.setAnimationEndCallback(() -> { + if (parentBeforePip != null) { + // TODO b/377362511: Animate local leash instead to also handle letterbox case. + // For multi-activity, set the crop to be null + finishTransaction.setCrop(pipLeash, null); + } + finishTransition(); + }); + cacheAndStartTransitionAnimator(animator); + saveReentryState(); + return true; + } + + private boolean startExpandToSplitAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); + + // Expanding PiP to Split-screen makes sense only if we are dealing with multi-activity PiP + // and the lastParentBeforePip is still in one of the split-stages. + // + // This means we should be animating the PiP activity leash, since we do the reparenting + // of the PiP activity back to its original task in startWCT. + TransitionInfo.Change pipChange = null; + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + // failsafe + if (pipChange == null || pipChange.getLeash() == null) { + return false; + } + mFinishCallback = finishCallback; + + // Get the original parent before PiP. If original task hosting the PiP activity was + // already visible, then it's not participating in this transition; in that case, + // parentBeforePip would be null. + final TransitionInfo.Change parentBeforePip = getChangeByToken(info, pipChange.getParent()); + + final Rect startBounds = pipChange.getStartAbsBounds(); + final Rect endBounds = pipChange.getEndAbsBounds(); + if (parentBeforePip != null) { + // Since we have the parent task amongst the targets, all PiP activity + // leash translations will be relative to the original task, NOT the root leash. + startBounds.offset(-parentBeforePip.getStartAbsBounds().left, + -parentBeforePip.getStartAbsBounds().top); + endBounds.offset(-parentBeforePip.getEndAbsBounds().left, + -parentBeforePip.getEndAbsBounds().top); + } + + final SurfaceControl pipLeash = pipChange.getLeash(); + PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash, + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + null /* srcRectHint */, ROTATION_0 /* delta */); + + + mSplitScreenControllerOptional.ifPresent(splitController -> { + splitController.finishEnterSplitScreen(finishTransaction); + }); + + animator.setAnimationEndCallback(() -> { + if (parentBeforePip == null) { + // After PipExpandAnimator is done modifying finishTransaction, we need to make + // sure PiP activity leash is offset at origin relative to its task as we reparent + // targets back from the transition root leash. + finishTransaction.setPosition(pipLeash, 0, 0); + } + finishTransition(); + }); + cacheAndStartTransitionAnimator(animator); + saveReentryState(); + return true; + } + + private void finishTransition() { + final int currentState = mPipTransitionState.getState(); + if (currentState != PipTransitionState.EXITING_PIP) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Unexpected state %s as we are finishing an exit-via-expand transition", + mPipTransitionState); + } + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + + if (mFinishCallback != null) { + // Need to unset mFinishCallback first because onTransitionFinished can re-enter this + // handler if there is a pending PiP animation. + final Transitions.TransitionFinishCallback finishCallback = mFinishCallback; + mFinishCallback = null; + finishCallback.onTransitionFinished(null /* finishWct */); + } + } + + private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { + final Rect endBounds = outPipTaskChange.getEndAbsBounds(); + final int width = endBounds.width(); + final int height = endBounds.height(); + final int left = endBounds.left; + final int top = endBounds.top; + int newTop, newLeft; + + if (delta == Surface.ROTATION_90) { + newLeft = top; + newTop = -(left + width); + } else { + newLeft = -(height + top); + newTop = left; + } + // Modify the endBounds, rotating and placing them potentially off-screen, so that + // as we translate and rotate around the origin, we place them right into the target. + endBounds.set(newLeft, newTop, newLeft + height, newTop + width); + } + + private void saveReentryState() { + float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mPipBoundsState.getBounds()); + mPipBoundsState.saveReentryState(snapFraction); + } + + private void cacheAndStartTransitionAnimator(@NonNull ValueAnimator animator) { + mTransitionAnimator = animator; + mTransitionAnimator.start(); + } + + @VisibleForTesting + interface PipExpandAnimatorSupplier { + PipExpandAnimator get(Context context, + @NonNull SurfaceControl leash, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + @NonNull Rect baseBounds, + @NonNull Rect startBounds, + @NonNull Rect endBounds, + @Nullable Rect sourceRectHint, + @Surface.Rotation int rotation); + } + + @VisibleForTesting + void setPipExpandAnimatorSupplier(@NonNull PipExpandAnimatorSupplier supplier) { + mPipExpandAnimatorSupplier = supplier; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java new file mode 100644 index 000000000000..01cda6c91108 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 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.pip2.phone.transition; + +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Surface.ROTATION_0; + +import android.annotation.NonNull; +import android.app.PictureInPictureParams; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; + +/** + * A set of utility methods to help resolve PiP transitions. + */ +public class PipTransitionUtils { + + /** + * @return change for a pinned mode task; null if no such task is in the list of changes. + */ + @Nullable + public static TransitionInfo.Change getPipChange(TransitionInfo info) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { + return change; + } + } + return null; + } + + /** + * @return change for a task with the provided token; null if no task with such token found. + */ + @Nullable + public static TransitionInfo.Change getChangeByToken(TransitionInfo info, + WindowContainerToken token) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getToken().equals(token)) { + return change; + } + } + return null; + } + + /** + * @return the leash to interact with the container this change represents. + * @throws NullPointerException if the leash is null. + */ + @NonNull + public static SurfaceControl getLeash(TransitionInfo.Change change) { + SurfaceControl leash = change.getLeash(); + Preconditions.checkNotNull(leash, "Leash is null for change=" + change); + return leash; + } + + /** + * Get the rotation delta in a potential fixed rotation transition. + * + * Whenever PiP participates in fixed rotation, its actual orientation isn't updated + * in the initial transition as per the async rotation convention. + * + * @param pipChange PiP change to verify that PiP task's rotation wasn't updated already. + * @param pipDisplayLayoutState display layout state that PiP component keeps track of. + */ + @Surface.Rotation + public static int getFixedRotationDelta(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change pipChange, + @NonNull PipDisplayLayoutState pipDisplayLayoutState) { + TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + int startRotation = pipChange.getStartRotation(); + if (pipChange.getEndRotation() != ROTATION_UNDEFINED + && startRotation != pipChange.getEndRotation()) { + // If PiP change was collected along with the display change and the orientation change + // happened in sync with the PiP change, then do not treat this as fixed-rotation case. + return ROTATION_0; + } + + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : pipDisplayLayoutState.getRotation(); + int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 + : startRotation - endRotation; + return delta; + } + + /** + * Gets a change amongst the transition targets that is in a different final orientation than + * the display, signalling a potential fixed rotation transition. + */ + @Nullable + public static TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { + return change; + } + } + return null; + } + + /** + * @return {@link PictureInPictureParams} provided by the client from the PiP change. + */ + @NonNull + public static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) { + return pipChange.getTaskInfo().pictureInPictureParams != null + ? pipChange.getTaskInfo().pictureInPictureParams + : new PictureInPictureParams.Builder().build(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index a969845fb8e8..847a0383e7d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -796,7 +796,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " unhandled root taskId=%d", taskInfo.taskId); } - } else if (TransitionUtil.isDividerBar(change)) { + } else if (TransitionUtil.isDividerBar(change) + || TransitionUtil.isDimLayer(change)) { final RemoteAnimationTarget target = TransitionUtil.newTarget(change, belowLayers - i, info, t, mLeashMap); // Add this as a app and we will separate them on launcher side by window type. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index a799b7f2580e..73b42d6f007c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -40,11 +40,13 @@ import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX; +import static com.android.wm.shell.common.split.SplitLayout.RESTING_DIM_LAYER; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIM_LAYER; import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; @@ -1824,6 +1826,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Ensure divider surface are re-parented back into the hierarchy at the end of the // transition. See Transition#buildFinishTransaction for more detail. finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); + if (Flags.enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().forEach(stage -> { + finishT.reparent(stage.mDimLayer, stage.mRootLeash); + }); + } else if (Flags.enableFlexibleTwoAppSplit()) { + finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash); + finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash); + } updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); finishT.show(mRootTaskLeash); @@ -3540,6 +3550,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, finishEnterSplitScreen(finishT); addDividerBarToTransition(info, true /* show */); + if (Flags.enableFlexibleTwoAppSplit()) { + addAllDimLayersToTransition(info, true /* show */); + } return true; } @@ -3790,6 +3803,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } addDividerBarToTransition(info, false /* show */); + if (Flags.enableFlexibleTwoAppSplit()) { + addAllDimLayersToTransition(info, false /* show */); + } } /** Call this when the recents animation canceled during split-screen. */ @@ -3836,6 +3852,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, returnToApp); mPausingTasks.clear(); if (returnToApp) { + // Reparent auxiliary surfaces (divider bar and dim layers) back onto their + // original roots. + if (Flags.enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().forEach(stage -> { + finishT.reparent(stage.mDimLayer, stage.mRootLeash); + finishT.setLayer(stage.mDimLayer, RESTING_DIM_LAYER); + }); + } else if (Flags.enableFlexibleTwoAppSplit()) { + finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash); + finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash); + finishT.setLayer(mMainStage.mDimLayer, RESTING_DIM_LAYER); + finishT.setLayer(mSideStage.mDimLayer, RESTING_DIM_LAYER); + } updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); @@ -3902,6 +3931,39 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, info.addChange(barChange); } + /** Add dim layers to the transition, so that they can be hidden/shown when animation starts. */ + private void addAllDimLayersToTransition(@NonNull TransitionInfo info, boolean show) { + if (Flags.enableFlexibleSplit()) { + List<StageTaskListener> stages = mStageOrderOperator.getActiveStages(); + for (int i = 0; i < stages.size(); i++) { + final StageTaskListener stage = stages.get(i); + mSplitState.getCurrentLayout().get(i).roundOut(mTempRect1); + addDimLayerToTransition(info, show, stage, mTempRect1); + } + } else { + addDimLayerToTransition(info, show, mMainStage, getMainStageBounds()); + addDimLayerToTransition(info, show, mSideStage, getSideStageBounds()); + } + } + + /** Adds a single dim layer to the given TransitionInfo. */ + private void addDimLayerToTransition(@NonNull TransitionInfo info, boolean show, + StageTaskListener stage, Rect bounds) { + final SurfaceControl dimLayer = stage.mDimLayer; + if (dimLayer == null || !dimLayer.isValid()) { + Slog.w(TAG, "addDimLayerToTransition but leash was released or not created"); + } else { + final TransitionInfo.Change change = + new TransitionInfo.Change(null /* token */, dimLayer); + change.setParent(mRootTaskInfo.token); + change.setStartAbsBounds(bounds); + change.setEndAbsBounds(bounds); + change.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK); + change.setFlags(FLAG_IS_DIM_LAYER); + info.addChange(change); + } + } + @NeverCompile @Override public void dump(@NonNull PrintWriter pw, String prefix) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 7aa00370ff58..dd5439a8aa10 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -389,7 +389,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT } else if (id == R.id.back_button) { mTaskOperations.injectBackKey(mDisplayId); } else if (id == R.id.minimize_window) { - mTaskOperations.minimizeTask(mTaskToken); + // This minimize button uses the same effect for any minimization. The last argument + // doesn't matter. + mTaskOperations.minimizeTask(mTaskToken, mTaskId, /* isLastTask= */ false); } else if (id == R.id.maximize_window) { RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); final DisplayAreaInfo rootDisplayAreaInfo = 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 1cc04b421132..5a6ea214e561 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 @@ -149,6 +149,8 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.extension.InsetsStateKt; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; +import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel; +import com.android.wm.shell.windowdecor.tiling.SnapEventHandler; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Pair; @@ -173,7 +175,7 @@ import java.util.function.Supplier; */ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, - FocusTransitionListener { + FocusTransitionListener, SnapEventHandler { private static final String TAG = "DesktopModeWindowDecorViewModel"; private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory; @@ -255,6 +257,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final WindowDecorTaskResourceLoader mTaskResourceLoader; private final RecentsTransitionHandler mRecentsTransitionHandler; private final DesktopModeCompatPolicy mDesktopModeCompatPolicy; + private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel; public DesktopModeWindowDecorViewModel( Context context, @@ -292,7 +295,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, - DesktopModeCompatPolicy desktopModeCompatPolicy) { + DesktopModeCompatPolicy desktopModeCompatPolicy, + DesktopTilingDecorViewModel desktopTilingDecorViewModel) { this( context, shellExecutor, @@ -335,7 +339,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, - desktopModeCompatPolicy); + desktopModeCompatPolicy, + desktopTilingDecorViewModel); } @VisibleForTesting @@ -381,7 +386,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, - DesktopModeCompatPolicy desktopModeCompatPolicy) { + DesktopModeCompatPolicy desktopModeCompatPolicy, + DesktopTilingDecorViewModel desktopTilingDecorViewModel) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -452,7 +458,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mTaskResourceLoader = taskResourceLoader; mRecentsTransitionHandler = recentsTransitionHandler; mDesktopModeCompatPolicy = desktopModeCompatPolicy; - + mDesktopTilingDecorViewModel = desktopTilingDecorViewModel; + mDesktopTasksController.setSnapEventHandler(this); shellInit.addInitCallback(this::onInit, this); } @@ -723,8 +730,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.mTaskInfo, left ? SnapPosition.LEFT : SnapPosition.RIGHT, left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU, - inputMethod, - decoration); + inputMethod); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); @@ -885,6 +891,33 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return snapshotList; } + @Override + public boolean snapToHalfScreen(@NonNull RunningTaskInfo taskInfo, + @NonNull Rect currentDragBounds, @NonNull SnapPosition position) { + return mDesktopTilingDecorViewModel.snapToHalfScreen(taskInfo, + mWindowDecorByTaskId.get(taskInfo.taskId), position, currentDragBounds); + } + + @Override + public void removeTaskIfTiled(int displayId, int taskId) { + mDesktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId); + } + + @Override + public void onUserChange() { + mDesktopTilingDecorViewModel.onUserChange(); + } + + @Override + public void onOverviewAnimationStateChange(boolean running) { + mDesktopTilingDecorViewModel.onOverviewAnimationStateChange(running); + } + + @Override + public boolean moveTaskToFrontIfTiled(@NonNull RunningTaskInfo taskInfo) { + return mDesktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo); + } + private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { @@ -951,7 +984,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTasksController.onDesktopWindowClose( wct, mDisplayId, decoration.mTaskInfo); final IBinder transition = mTaskOperations.closeTask(mTaskToken, wct); - if (transition != null && runOnTransitionStart != null) { + if (transition != null) { runOnTransitionStart.invoke(transition); } } @@ -1238,8 +1271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, taskInfo, decoration.mTaskSurface, new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), newTaskBounds, decoration.calculateValidDragArea(), - new Rect(mOnDragStartInitialBounds), e, - mWindowDecorByTaskId.get(taskInfo.taskId)); + new Rect(mOnDragStartInitialBounds), e); if (touchingButton) { // We need the input event to not be consumed here to end the ripple // effect on the touched button. We will reset drag state in the ensuing @@ -1444,16 +1476,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); boolean dragFromStatusBarAllowed = false; final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); - if (DesktopModeStatus.canEnterDesktopMode(mContext)) { + if (DesktopModeStatus.canEnterDesktopMode(mContext) + || BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { // In proto2 any full screen or multi-window task can be dragged to // freeform. dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } - if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { - // TODO(b/388851898): add support for split screen (multi-window wm mode) - dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN; - } final boolean shouldStartTransitionDrag = relevantDecor.checkTouchEventInFocusedCaptionHandle(ev) || DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue(); @@ -1502,7 +1531,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // Do not create an indicator at all if we're not past transition height. DisplayLayout layout = mDisplayController .getDisplayLayout(relevantDecor.mTaskInfo.displayId); - if (ev.getRawY() < 2 * layout.stableInsets().top + // It's possible task is not at the top of the screen (e.g. bottom of vertical + // Splitscreen) + final int taskTop = relevantDecor.mTaskInfo.configuration.windowConfiguration + .getBounds().top; + if (ev.getRawY() < 2 * layout.stableInsets().top + taskTop && mMoveToDesktopAnimator == null) { return; } 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 3fb94630eab3..271dead467b4 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 @@ -803,8 +803,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!mTaskInfo.isVisible()) { closeMaximizeMenu(); } else { - final int menuWidth = calculateMaximizeMenuWidth(); - mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(menuWidth), startT); + mMaximizeMenu.positionMenu(startT); } } @@ -1069,27 +1068,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Resources.ID_NULL; } - private int calculateMaximizeMenuWidth() { - final boolean showImmersive = DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() - && TaskInfoKt.getRequestingImmersive(mTaskInfo); - final boolean showMaximize = true; - final boolean showSnaps = mTaskInfo.isResizeable; - int showCount = 0; - if (showImmersive) showCount++; - if (showMaximize) showCount++; - if (showSnaps) showCount++; - return switch (showCount) { - case 1 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_one_options); - case 2 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_two_options); - case 3 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_three_options); - default -> throw new IllegalArgumentException(""); - }; - } - - private PointF calculateMaximizeMenuPosition(int menuWidth) { + private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) { final PointF position = new PointF(); final Resources resources = mContext.getResources(); final DisplayLayout displayLayout = @@ -1105,9 +1084,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int[] maximizeButtonLocation = new int[2]; maximizeWindowButton.getLocationInWindow(maximizeButtonLocation); - final int menuHeight = loadDimensionPixelSize( - resources, R.dimen.desktop_mode_maximize_menu_height); - float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth - maximizeWindowButton.getWidth()) / 2)); float menuTop = (mPositionInParent.y + captionHeight); @@ -1294,17 +1270,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display maximize menu window */ void createMaximizeMenu() { - final int menuWidth = calculateMaximizeMenuWidth(); mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, mDisplayController, mTaskInfo, mContext, - calculateMaximizeMenuPosition(menuWidth), mSurfaceControlTransactionSupplier); + (width, height) -> calculateMaximizeMenuPosition(width, height), + mSurfaceControlTransactionSupplier); mMaximizeMenu.show( /* isTaskInImmersiveMode= */ DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() && mDesktopUserRepositories.getProfile(mTaskInfo.userId) .isTaskInFullImmersiveState(mTaskInfo.taskId), - /* menuWidth= */ menuWidth, /* showImmersiveOption= */ DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() && TaskInfoKt.getRequestingImmersive(mTaskInfo), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 38accce82999..ad3525af3f94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -90,7 +90,7 @@ class MaximizeMenu( private val displayController: DisplayController, private val taskInfo: RunningTaskInfo, private val decorWindowContext: Context, - private val menuPosition: PointF, + private val positionSupplier: (Int, Int) -> PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } ) { private var maximizeMenu: AdditionalViewHostViewContainer? = null @@ -100,19 +100,19 @@ class MaximizeMenu( private val cornerRadius = loadDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_corner_radius ).toFloat() - private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height) + private lateinit var menuPosition: PointF private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding) /** Position the menu relative to the caption's position. */ - fun positionMenu(position: PointF, t: Transaction) { - menuPosition.set(position) + fun positionMenu(t: Transaction) { + menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0, + maximizeMenuView?.measureHeight() ?: 0) t.setPosition(leash, menuPosition.x, menuPosition.y) } /** Creates and shows the maximize window. */ fun show( isTaskInImmersiveMode: Boolean, - menuWidth: Int, showImmersiveOption: Boolean, showSnapOptions: Boolean, onMaximizeOrRestoreClickListener: () -> Unit, @@ -125,7 +125,6 @@ class MaximizeMenu( if (maximizeMenu != null) return createMaximizeMenu( isTaskInImmersiveMode = isTaskInImmersiveMode, - menuWidth = menuWidth, showImmersiveOption = showImmersiveOption, showSnapOptions = showSnapOptions, onMaximizeClickListener = onMaximizeOrRestoreClickListener, @@ -161,7 +160,6 @@ class MaximizeMenu( /** Create a maximize menu that is attached to the display area. */ private fun createMaximizeMenu( isTaskInImmersiveMode: Boolean, - menuWidth: Int, showImmersiveOption: Boolean, showSnapOptions: Boolean, onMaximizeClickListener: () -> Unit, @@ -178,16 +176,6 @@ class MaximizeMenu( .setName("Maximize Menu") .setContainerLayer() .build() - val lp = WindowManager.LayoutParams( - menuWidth, - menuHeight, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, - PixelFormat.TRANSPARENT - ) - lp.title = "Maximize Menu for Task=" + taskInfo.taskId - lp.setTrustedOverlay() val windowManager = WindowlessWindowManager( taskInfo.configuration, leash, @@ -207,7 +195,6 @@ class MaximizeMenu( MaximizeMenuView.ImmersiveConfig.Hidden }, showSnapOptions = showSnapOptions, - menuHeight = menuHeight, menuPadding = menuPadding, ).also { menuView -> menuView.bind(taskInfo) @@ -217,6 +204,19 @@ class MaximizeMenu( menuView.onRightSnapClickListener = onRightSnapClickListener menuView.onMenuHoverListener = onHoverListener menuView.onOutsideTouchListener = onOutsideTouchListener + val menuWidth = menuView.measureWidth() + val menuHeight = menuView.measureHeight() + menuPosition = positionSupplier(menuWidth, menuHeight) + val lp = WindowManager.LayoutParams( + menuWidth.toInt(), + menuHeight.toInt(), + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + PixelFormat.TRANSPARENT + ) + lp.title = "Maximize Menu for Task=" + taskInfo.taskId + lp.setTrustedOverlay() viewHost.setView(menuView.rootView, lp) } @@ -268,7 +268,6 @@ class MaximizeMenu( private val sizeToggleDirection: SizeToggleDirection, immersiveConfig: ImmersiveConfig, showSnapOptions: Boolean, - private val menuHeight: Int, private val menuPadding: Int ) { val rootView = LayoutInflater.from(context) @@ -583,7 +582,7 @@ class MaximizeMenu( // the menu. val value = animatedValue as Float val topPadding = menuPadding - - ((1 - value) * menuHeight).toInt() + ((1 - value) * measureHeight()).toInt() container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } @@ -604,7 +603,7 @@ class MaximizeMenu( } }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, - (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { + (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight(), 0f).apply { duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE }, @@ -667,7 +666,7 @@ class MaximizeMenu( // the menu. val value = animatedValue as Float val topPadding = menuPadding - - ((1 - value) * menuHeight).toInt() + ((1 - value) * measureHeight()).toInt() container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } @@ -688,7 +687,7 @@ class MaximizeMenu( } }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, - 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight).apply { + 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight()).apply { duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = FAST_OUT_LINEAR_IN }, @@ -792,6 +791,18 @@ class MaximizeMenu( ) } + /** Measure width of the root view of this menu. */ + fun measureWidth() : Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + return rootView.getMeasuredWidth() + } + + /** Measure height of the root view of this menu. */ + fun measureHeight() : Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + return rootView.getMeasuredHeight() + } + private fun deactivateSnapOptions() { // TODO(b/346440693): the background/colorStateList set on these buttons is overridden // to a static resource & color on manually tracked hover events, which defeats the @@ -1036,7 +1047,7 @@ interface MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - menuPosition: PointF, + positionSupplier: (Int, Int) -> PointF, transactionSupplier: Supplier<Transaction> ): MaximizeMenu } @@ -1049,7 +1060,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - menuPosition: PointF, + positionSupplier: (Int, Int) -> PointF, transactionSupplier: Supplier<Transaction> ): MaximizeMenu { return MaximizeMenu( @@ -1058,7 +1069,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { displayController, taskInfo, decorWindowContext, - menuPosition, + positionSupplier, transactionSupplier ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java index bc85d2b40748..45ba4413814c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -86,14 +86,18 @@ class TaskOperations { return null; } - IBinder minimizeTask(WindowContainerToken taskToken) { - return minimizeTask(taskToken, new WindowContainerTransaction()); + IBinder minimizeTask(WindowContainerToken taskToken, int taskId, boolean isLastTask) { + return minimizeTask(taskToken, taskId, isLastTask, new WindowContainerTransaction()); } - IBinder minimizeTask(WindowContainerToken taskToken, WindowContainerTransaction wct) { + IBinder minimizeTask( + WindowContainerToken taskToken, + int taskId, + boolean isLastTask, + WindowContainerTransaction wct) { wct.reorder(taskToken, false); if (Transitions.ENABLE_SHELL_TRANSITIONS) { - return mTransitionStarter.startMinimizedModeTransition(wct); + return mTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask); } else { mSyncQueue.queue(wct); return null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 25dadfde274d..4002dc572897 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -361,6 +361,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } outResult.mRootView = rootView; + final boolean fontScaleChanged = mWindowDecorConfig != null + && mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale; final int oldDensityDpi = mWindowDecorConfig != null ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; final int oldNightMode = mWindowDecorConfig != null @@ -375,7 +377,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || mDisplay.getDisplayId() != mTaskInfo.displayId || oldLayoutResId != mLayoutResId || oldNightMode != newNightMode - || mDecorWindowContext == null) { + || mDecorWindowContext == null + || fontScaleChanged) { releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt index 4a09614029dc..a5592f81a39e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt @@ -50,9 +50,8 @@ class ReusableWindowDecorViewHost( @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter = SurfaceControlViewHostAdapter(context, display), + private val rootView: FrameLayout = FrameLayout(context) ) : WindowDecorViewHost, Warmable { - @VisibleForTesting val rootView = FrameLayout(context) - private var currentUpdateJob: Job? = null override val surfaceControl: SurfaceControl @@ -131,8 +130,10 @@ class ReusableWindowDecorViewHost( Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost") viewHostAdapter.prepareViewHost(configuration, touchableRegion) onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) } - rootView.removeAllViews() - rootView.addView(view) + if (view.parent != rootView) { + rootView.removeAllViews() + rootView.addView(view) + } viewHostAdapter.updateView(rootView, attrs) Trace.endSection() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt new file mode 100644 index 000000000000..52e24d6fe0d0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 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.windowdecor.tiling + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect +import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition + +/** Interface for handling snap to half screen events. */ +interface SnapEventHandler { + /** Snaps an app to half the screen for tiling. */ + fun snapToHalfScreen( + taskInfo: RunningTaskInfo, + currentDragBounds: Rect, + position: SnapPosition, + ): Boolean + + /** Removes a task from tiling if it's tiled, for example on task exiting. */ + fun removeTaskIfTiled(displayId: Int, taskId: Int) + + /** Notifies the tiling handler of user switch. */ + fun onUserChange() + + /** Notifies the tiling handler of overview animation state change. */ + fun onOverviewAnimationStateChange(running: Boolean) + + /** If a task is tiled, delegate moving to front to tiling infrastructure. */ + fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index 87ee4f58bfdd..42310caba1c6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -215,7 +215,7 @@ public class BubbleTransitionsTest extends ShellTestCase { pendingWct.reorder(pendingDragOpToken, /* onTop= */ false); BubbleTransitions.DragData dragData = new BubbleTransitions.DragData( - draggedTaskBounds, pendingWct + draggedTaskBounds, pendingWct, /* releasedOnLeft= */ false ); final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java index d3de0f7c09b4..3d5e9495e29d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -16,26 +16,52 @@ package com.android.wm.shell.common; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import android.content.Context; +import android.content.res.Configuration; +import android.graphics.RectF; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; +import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; +import android.testing.TestableContext; +import android.util.SparseArray; +import android.view.Display; +import android.view.DisplayAdjustments; +import android.view.IDisplayWindowListener; import android.view.IWindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestSyncExecutor; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.quality.Strictness; + +import java.util.function.Consumer; /** * Tests for the display controller. @@ -46,23 +72,163 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class DisplayControllerTests extends ShellTestCase { - - private @Mock Context mContext; - private @Mock IWindowManager mWM; - private @Mock ShellInit mShellInit; - private @Mock ShellExecutor mMainExecutor; - private @Mock DisplayManager mDisplayManager; + @Mock private IWindowManager mWM; + @Mock private ShellInit mShellInit; + @Mock private DisplayManager mDisplayManager; + @Mock private DisplayTopology mMockTopology; + @Mock private DisplayController.OnDisplaysChangedListener mListener; + private StaticMockitoSession mMockitoSession; + private TestSyncExecutor mMainExecutor; + private IDisplayWindowListener mDisplayContainerListener; + private Consumer<DisplayTopology> mCapturedTopologyListener; + private Display mMockDisplay; private DisplayController mController; + private static final int DISPLAY_ID_0 = 0; + private static final int DISPLAY_ID_1 = 1; + private static final RectF DISPLAY_ABS_BOUNDS_0 = new RectF(10, 10, 20, 20); + private static final RectF DISPLAY_ABS_BOUNDS_1 = new RectF(11, 11, 22, 22); @Before - public void setUp() { - MockitoAnnotations.initMocks(this); + public void setUp() throws RemoteException { + mMockitoSession = + ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(DesktopModeStatus.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + mContext = spy(new TestableContext( + androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() + .getContext(), null)); + + mMainExecutor = new TestSyncExecutor(); mController = new DisplayController( mContext, mWM, mShellInit, mMainExecutor, mDisplayManager); + + mMockDisplay = mock(Display.class); + when(mMockDisplay.getDisplayAdjustments()).thenReturn( + new DisplayAdjustments(new Configuration())); + when(mDisplayManager.getDisplay(anyInt())).thenReturn(mMockDisplay); + when(mDisplayManager.getDisplayTopology()).thenReturn(mMockTopology); + doAnswer(invocation -> { + mDisplayContainerListener = invocation.getArgument(0); + return new int[]{DISPLAY_ID_0}; + }).when(mWM).registerDisplayWindowListener(any()); + doAnswer(invocation -> { + mCapturedTopologyListener = invocation.getArgument(1); + return null; + }).when(mDisplayManager).registerTopologyListener(any(), any()); + SparseArray<RectF> absoluteBounds = new SparseArray<>(); + absoluteBounds.put(DISPLAY_ID_0, DISPLAY_ABS_BOUNDS_0); + absoluteBounds.put(DISPLAY_ID_1, DISPLAY_ABS_BOUNDS_1); + when(mMockTopology.getAbsoluteBounds()).thenReturn(absoluteBounds); + } + + @After + public void tearDown() { + if (mMockitoSession != null) { + mMockitoSession.finishMocking(); + } } @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), eq(mController)); } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onInit_canEnterDesktopMode_registerListeners() throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + + assertNotNull(mController.getDisplayContext(DISPLAY_ID_0)); + verify(mWM).registerDisplayWindowListener(any()); + verify(mDisplayManager).registerTopologyListener(eq(mMainExecutor), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onInit_canNotEnterDesktopMode_onlyRegisterDisplayWindowListener() + throws RemoteException { + ExtendedMockito.doReturn(false) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + + assertNotNull(mController.getDisplayContext(DISPLAY_ID_0)); + verify(mWM).registerDisplayWindowListener(any()); + verify(mDisplayManager, never()).registerTopologyListener(any(), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void addDisplayWindowListener_notifiesExistingDisplaysAndTopology() { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0)); + verify(mListener).onTopologyChanged(eq(mMockTopology)); + } + + @Test + public void onDisplayAddedAndRemoved_updatesDisplayContexts() throws RemoteException { + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0)); + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_1)); + assertNotNull(mController.getDisplayContext(DISPLAY_ID_1)); + verify(mContext).createDisplayContext(eq(mMockDisplay)); + + mDisplayContainerListener.onDisplayRemoved(DISPLAY_ID_1); + + assertNull(mController.getDisplayContext(DISPLAY_ID_1)); + verify(mListener).onDisplayRemoved(eq(DISPLAY_ID_1)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onDisplayTopologyChanged_updateDisplayLayout() throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mController.onInit(); + mController.addDisplayWindowListener(mListener); + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + mCapturedTopologyListener.accept(mMockTopology); + + assertEquals(DISPLAY_ABS_BOUNDS_0, mController.getDisplayLayout(DISPLAY_ID_0) + .globalBoundsDp()); + assertEquals(DISPLAY_ABS_BOUNDS_1, mController.getDisplayLayout(DISPLAY_ID_1) + .globalBoundsDp()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onDisplayTopologyChanged_topologyBeforeDisplayAdded_appliesBoundsOnAdd() + throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + mCapturedTopologyListener.accept(mMockTopology); + + assertNull(mController.getDisplayLayout(DISPLAY_ID_1)); + + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + assertEquals(DISPLAY_ABS_BOUNDS_0, + mController.getDisplayLayout(DISPLAY_ID_0).globalBoundsDp()); + assertEquals(DISPLAY_ABS_BOUNDS_1, + mController.getDisplayLayout(DISPLAY_ID_1).globalBoundsDp()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt new file mode 100644 index 000000000000..c91ef5e6b868 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 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.common + +import android.testing.AndroidTestingRunner +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [WindowContainerTransactionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:WindowContainerTransactionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class WindowContainerTransactionSupplierTest { + + @Test + fun `WindowContainerTransactionSupplier supplies a WindowContainerTransaction`() { + val supplier = WindowContainerTransactionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is WindowContainerTransaction + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java index e3798e92c092..a6c35f1bd93c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -29,9 +29,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsState; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java index 85f1da5322ea..737735c9efcd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static org.mockito.Mockito.when; @@ -27,9 +27,6 @@ import android.view.DisplayInfo; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Assert; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java index 080b0ae006ea..6bda2259b44c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -32,13 +32,6 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipBoundsAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface; -import com.android.wm.shell.common.pip.PipSnapAlgorithm; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java index 304da75f870c..ad664acfdc37 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -36,10 +36,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.function.TriConsumer; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java index b583acda1c9a..1756aad8fc9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; @@ -29,8 +29,6 @@ import android.graphics.Rect; import android.testing.AndroidTestingRunner; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDoubleTapHelper; import org.junit.Assert; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java index ac13d7ffcd61..3e71ab3e1ad4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; @@ -25,8 +25,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipSnapAlgorithm; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java new file mode 100644 index 000000000000..22a85fc49a4b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2025 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.common.split; + +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; + +import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.graphics.Rect; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class FlexParallaxSpecTests { + ParallaxSpec mFlexSpec = new FlexParallaxSpec(); + + Rect mDisplayBounds = new Rect(0, 0, 1000, 1000); + Rect mRetreatingSurface = new Rect(0, 0, 1000, 1000); + Rect mRetreatingContent = new Rect(0, 0, 1000, 1000); + Rect mAdvancingSurface = new Rect(0, 0, 1000, 1000); + Rect mAdvancingContent = new Rect(0, 0, 1000, 1000); + boolean mIsLeftRightSplit; + boolean mTopLeftShrink; + + int mDimmingSide; + float mDimValue; + Point mRetreatingParallax = new Point(0, 0); + Point mAdvancingParallax = new Point(0, 0); + + @Mock DividerSnapAlgorithm mockSnapAlgorithm; + @Mock SnapTarget mockStartEdge; + @Mock SnapTarget mockFirstTarget; + @Mock SnapTarget mockMiddleTarget; + @Mock SnapTarget mockLastTarget; + @Mock SnapTarget mockEndEdge; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mockSnapAlgorithm.getDismissStartTarget()).thenReturn(mockStartEdge); + when(mockSnapAlgorithm.getFirstSplitTarget()).thenReturn(mockFirstTarget); + when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget); + when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget); + when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge); + + when(mockStartEdge.getPosition()).thenReturn(0); + when(mockFirstTarget.getPosition()).thenReturn(250); + when(mockMiddleTarget.getPosition()).thenReturn(500); + when(mockLastTarget.getPosition()).thenReturn(750); + when(mockEndEdge.getPosition()).thenReturn(1000); + } + + @Test + public void testHorizontalDragFromCenter() { + mIsLeftRightSplit = true; + simulateDragFromCenterToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToLeft(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + @Test + public void testHorizontalDragFromLeft() { + mIsLeftRightSplit = true; + simulateDragFromLeftToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToCenter(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToCenter(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isGreaterThan(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + @Test + public void testHorizontalDragFromRight() { + mIsLeftRightSplit = true; + + simulateDragFromRightToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToLeft(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToCenter(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToCenter(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + private void simulateDragFromCenterToLeft(int to) { + int from = 500; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = onscreenAppRight(to); + mAdvancingContent = onscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromCenterToRight(int to) { + int from = 500; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = onscreenAppLeft(to); + mAdvancingContent = onscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToLeft(int to) { + int from = 250; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = fullOffscreenAppLeft(from); + mAdvancingSurface = onscreenAppRight(to); + mAdvancingContent = onscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToCenter(int to) { + int from = 250; + + mRetreatingSurface = onscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = fullOffscreenAppLeft(to); + mAdvancingContent = fullOffscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToRight(int to) { + int from = 250; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = fullOffscreenAppLeft(to); + mAdvancingContent = fullOffscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToLeft(int to) { + int from = 750; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = fullOffscreenAppRight(to); + mAdvancingContent = fullOffscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToCenter(int to) { + int from = 750; + + mRetreatingSurface = onscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = fullOffscreenAppRight(to); + mAdvancingContent = fullOffscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToRight(int to) { + int from = 750; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = fullOffscreenAppRight(from); + mAdvancingSurface = onscreenAppLeft(to); + mAdvancingContent = onscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private Rect flexOffscreenAppLeft(int pos) { + return new Rect(pos - (1000 - pos), 0, pos, 1000); + } + + private Rect onscreenAppLeft(int pos) { + return new Rect(0, 0, pos, 1000); + } + + private Rect fullOffscreenAppLeft(int pos) { + return new Rect(Math.min(0, pos - 750), 0, pos, 1000); + } + + private Rect flexOffscreenAppRight(int pos) { + return new Rect(pos, 0, pos * 2, 1000); + } + + private Rect onscreenAppRight(int pos) { + return new Rect(pos, 0, 1000, 1000); + } + + private Rect fullOffscreenAppRight(int pos) { + return new Rect(pos, 0, Math.max(pos + 750, 1000), 1000); + } + + private void calculateDimAndParallax(int from, int to) { + resetParallax(); + mTopLeftShrink = to < from; + mDimmingSide = mFlexSpec.getDimmingSide(to, mockSnapAlgorithm, mIsLeftRightSplit); + mDimValue = mFlexSpec.getDimValue(to, mockSnapAlgorithm); + mFlexSpec.getParallax(mRetreatingParallax, mAdvancingParallax, to, mockSnapAlgorithm, + mIsLeftRightSplit, mDisplayBounds, mRetreatingSurface, mRetreatingContent, + mAdvancingSurface, mAdvancingContent, mDimmingSide, mTopLeftShrink); + } + + private void resetParallax() { + mRetreatingParallax.set(0, 0); + mAdvancingParallax.set(0, 0); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt new file mode 100644 index 000000000000..a5f6ced20dc0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2025 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.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListenerFactory]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerFactoryTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerFactoryTest : ShellTestCase() { + + @Test + fun `When invoked a ReachabilityGestureListenerFactory is created`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkReachabilityGestureListenerCreated() + } + } + + @Test + fun `Right parameters are used for creation`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkRightParamsAreUsed() + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerFactoryRobotTest>) { + val robot = ReachabilityGestureListenerFactoryRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerFactoryRobotTest { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val factory: ReachabilityGestureListenerFactory + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private lateinit var obtainedResult: Any + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + factory = ReachabilityGestureListenerFactory(transitions, animationHandler, wctSupplier) + } + + fun invokeCreate(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + obtainedResult = factory.createReachabilityGestureListener(taskId, token) + } + + fun checkReachabilityGestureListenerCreated(expected: Boolean = true) { + assertEquals(expected, obtainedResult is ReachabilityGestureListener) + } + + fun checkRightParamsAreUsed(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + with(obtainedResult as ReachabilityGestureListener) { + // Click outside the bounds + updateActivityBounds(Rect(0, 0, 10, 20)) + onDoubleTap(motionEventAt(50f, 100f)) + // WindowContainerTransactionSupplier is invoked to create a + // WindowContainerTransaction + verify(wctSupplier).get() + // Verify the right params are passed to startAppCompatReachability() + verify(wct).setReachabilityOffset( + token!!, + taskId, + 50, + 100 + ) + // startTransition() is invoked on Transitions with the right parameters + verify(transitions).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt new file mode 100644 index 000000000000..bc10ea578ffb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2025 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.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.compatui.letterbox.asMode +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListener]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerTest : ShellTestCase() { + + @Test + fun `Only events outside the bounds are handled`() { + runTestScenario { r -> + r.updateActivityBounds(Rect(0, 0, 100, 200)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = false, 50, 100) + r.verifyReachabilityTransitionStarted(expected = false) + r.verifyEventIsHandled(expected = false) + + r.updateActivityBounds(Rect(0, 0, 10, 50)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = true, 50, 100) + r.verifyReachabilityTransitionStarted(expected = true) + r.verifyEventIsHandled(expected = true) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerRobotTest>) { + val robot = ReachabilityGestureListenerRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerRobotTest( + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val reachabilityListener: ReachabilityGestureListener + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private var eventHandled = false + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + reachabilityListener = + ReachabilityGestureListener( + taskId, + token, + transitions, + animationHandler, + wctSupplier + ) + } + + fun updateActivityBounds(activityBounds: Rect) { + reachabilityListener.updateActivityBounds(activityBounds) + } + + fun sendMotionEvent(x: Int, y: Int) { + eventHandled = reachabilityListener.onDoubleTap(motionEventAt(x.toFloat(), y.toFloat())) + } + + fun verifyReachabilityTransitionCreated( + expected: Boolean, + x: Int, + y: Int, + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + verify(wct, expected.asMode()).setReachabilityOffset( + token!!, + taskId, + x, + y + ) + } + + fun verifyReachabilityTransitionStarted(expected: Boolean = true) { + verify(transitions, expected.asMode()).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + + fun verifyEventIsHandled(expected: Boolean) { + assertEquals(expected, eventHandled) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index 0b41952a89d7..e9f92cfd7c56 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -58,6 +58,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito @@ -126,17 +127,6 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test - fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() { - val wct = WindowContainerTransaction() - whenever(freeformTaskTransitionHandler.startMinimizedModeTransition(any())) - .thenReturn(mock()) - - mixedHandler.startMinimizedModeTransition(wct) - - verify(freeformTaskTransitionHandler).startMinimizedModeTransition(wct) - } - - @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX) fun startRemoveTransition_callsFreeformTaskTransitionHandler() { val wct = WindowContainerTransaction() @@ -531,6 +521,131 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX) + fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsDisabled_doesNotUseMixedHandler() { + val wct = WindowContainerTransaction() + val task = createTask(WINDOWING_MODE_FREEFORM) + whenever( + freeformTaskTransitionHandler.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(mock()) + + mixedHandler.startMinimizedModeTransition( + wct = wct, + taskId = task.taskId, + isLastTask = true, + ) + + verify(freeformTaskTransitionHandler) + .startMinimizedModeTransition(eq(wct), eq(task.taskId), eq(true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX) + fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsEnabled_notLastTask_callsMinimizationHandler() { + val wct = WindowContainerTransaction() + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTaskChange = createChange(minimizingTask) + val transition = Binder() + whenever( + transitions.startTransition(eq(Transitions.TRANSIT_MINIMIZE), eq(wct), anyOrNull()) + ) + .thenReturn(transition) + whenever( + desktopMinimizationTransitionHandler.startAnimation( + any(), + any(), + any(), + any(), + any(), + ) + ) + .thenReturn(true) + + mixedHandler.startMinimizedModeTransition( + wct = wct, + taskId = minimizingTask.taskId, + isLastTask = false, + ) + val started = + mixedHandler.startAnimation( + transition = transition, + info = + createCloseTransitionInfo( + Transitions.TRANSIT_MINIMIZE, + listOf(minimizingTaskChange), + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {}, + ) + + assertTrue("Should delegate animation to minimization transition handler", started) + verify(desktopMinimizationTransitionHandler) + .startAnimation( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), + any(), + any(), + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX) + fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsEnabled_withMinimizingLastTask_dispatchesTransition() { + val wct = WindowContainerTransaction() + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTaskChange = createChange(minimizingTask) + val transition = Binder() + whenever( + transitions.startTransition(eq(Transitions.TRANSIT_MINIMIZE), eq(wct), anyOrNull()) + ) + .thenReturn(transition) + whenever( + desktopMinimizationTransitionHandler.startAnimation( + any(), + any(), + any(), + any(), + any(), + ) + ) + .thenReturn(true) + + mixedHandler.startMinimizedModeTransition( + wct = wct, + taskId = minimizingTask.taskId, + isLastTask = true, + ) + mixedHandler.startAnimation( + transition = transition, + info = + createCloseTransitionInfo( + Transitions.TRANSIT_MINIMIZE, + listOf(minimizingTaskChange), + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {}, + ) + + verify(transitions) + .dispatchTransition( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), + any(), + any(), + eq(mixedHandler), + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { val wct = WindowContainerTransaction() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 8510441c0557..ed9b97d264f7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -55,6 +55,8 @@ import org.mockito.Mock import org.mockito.Mockito.inOrder import org.mockito.Mockito.spy import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -894,12 +896,12 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { val taskId = 1 val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.removeTask(THIRD_DISPLAY, taskId) assertThat(repo.isActiveTask(taskId)).isFalse() - assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) + assertThat(listener.activeChangesOnThirdDisplay).isEqualTo(2) } @Test @@ -917,7 +919,7 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { fun removeTask_updatesTaskVisibility() { repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY) val taskId = 1 - repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.removeTask(THIRD_DISPLAY, taskId) @@ -1106,6 +1108,30 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setTaskInFullImmersiveState_inDesk_savedAsInImmersiveState() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + assertThat(repo.isTaskInFullImmersiveState(6)).isFalse() + + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskInFullImmersiveState_inDesk_removedAsInImmersiveState() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = false) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse() + } + + @Test fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() { repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) @@ -1274,14 +1300,146 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = 8)) } + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setDeskActive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + + repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6) + + assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(6) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setDeskInactive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6) + + repo.setDeskInactive(deskId = 6) + + assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDeskIdForTask() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + assertThat(repo.getDeskIdForTask(10)).isEqualTo(6) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_clearsBoundsBeforeMaximize() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.saveBoundsBeforeMaximize(taskId = 10, bounds = Rect(10, 10, 100, 100)) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.removeBoundsBeforeMaximize(taskId = 10)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_clearsBoundsBeforeImmersive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.saveBoundsBeforeFullImmersive(taskId = 10, bounds = Rect(10, 10, 100, 100)) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.removeBoundsBeforeFullImmersive(taskId = 10)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromZOrderList() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.getFreeformTasksIdsInDeskInZOrder(deskId = 6)).doesNotContain(10) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromMinimized() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.minimizeTaskInDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.getMinimizedTaskIdsInDesk(deskId = 6)).doesNotContain(10) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromImmersive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromActiveTasks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isActiveTaskInDesk(taskId = 10, deskId = 6)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromVisibleTasks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isVisibleTaskInDesk(taskId = 10, deskId = 6)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeTaskFromDesk_updatesPersistence() = runTest { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + clearInvocations(persistentRepository) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + verify(persistentRepository) + .addOrUpdateDesktop( + userId = eq(DEFAULT_USER_ID), + desktopId = eq(6), + visibleTasks = any(), + minimizedTasks = any(), + freeformTasksInZOrder = any(), + ) + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 + var activeChangesOnThirdDisplay = 0 override fun onActiveTasksChanged(displayId: Int) { when (displayId) { DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++ SECOND_DISPLAY -> activeChangesOnSecondaryDisplay++ + THIRD_DISPLAY -> activeChangesOnThirdDisplay++ else -> fail("Active task listener received unexpected display id: $displayId") } } @@ -1290,9 +1448,11 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { class TestVisibilityListener : DesktopRepository.VisibleTasksListener { var visibleTasksCountOnDefaultDisplay = 0 var visibleTasksCountOnSecondaryDisplay = 0 + var visibleTasksCountOnThirdDisplay = 0 var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 + var visibleChangesOnThirdDisplay = 0 override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { when (displayId) { @@ -1304,6 +1464,10 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { visibleTasksCountOnSecondaryDisplay = visibleTasksCount visibleChangesOnSecondaryDisplay++ } + THIRD_DISPLAY -> { + visibleTasksCountOnThirdDisplay = visibleTasksCount + visibleChangesOnThirdDisplay++ + } else -> fail("Visible task listener received unexpected display id: $displayId") } } 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 3ee9501dd8dd..e2c3dda0d927 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 @@ -151,8 +151,7 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration -import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel +import com.android.wm.shell.windowdecor.tiling.SnapEventHandler import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import java.util.Optional @@ -179,6 +178,7 @@ import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock @@ -233,6 +233,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock lateinit var multiInstanceHelper: MultiInstanceHelper @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator @Mock lateinit var recentTasksController: RecentTasksController + @Mock lateinit var snapEventHandler: SnapEventHandler @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockSurface: SurfaceControl @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener @@ -245,9 +246,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer @Mock private lateinit var mockToast: Toast private lateinit var mockitoSession: StaticMockitoSession - @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel @Mock private lateinit var bubbleController: BubbleController - @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration @Mock private lateinit var resources: Resources @Mock lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener @@ -328,8 +327,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext)) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() } whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() } + whenever(exitDesktopTransitionHandler.startTransition(any(), any(), any(), any())) + .thenReturn(Binder()) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayController.getDisplayContext(anyInt())).thenReturn(mockDisplayContext) whenever(displayController.getDisplay(anyInt())).thenReturn(display) @@ -379,6 +380,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener + controller.setSnapEventHandler(snapEventHandler) assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -422,7 +424,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mockHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), overviewToDesktopTransitionObserver, @@ -918,7 +919,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) @@ -2040,6 +2044,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_fromDesk_reparentsToTaskDisplayArea() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + wct.assertHop(ReparentPredicate(token = task.token, parentToken = tda.token, toTop = true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_fromDesk_deactivatesDesk() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + verify(desksOrganizer).deactivateDesk(wct, deskId = 0) + } + + @Test fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -2054,6 +2082,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val homeTask = setUpHomeTask() @@ -2079,6 +2108,60 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity_multiDesksEnabled() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + setUpHomeTask() + val task = setUpFreeformTask() + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Removes wallpaper activity when leaving desktop + wct.assertReorder(wallpaperToken, toTop = false) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_homeBehindFullscreen_multiDesksEnabled() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToFullscreen_tdaFreeform_enforcedDesktop_doesNotReorderHome() { whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) @@ -2094,9 +2177,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = getLatestExitDesktopWct() verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - assertThat(wct.hierarchyOps).hasSize(1) // Removes wallpaper activity when leaving desktop but doesn't reorder home or the task - wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wct.assertReorder(wallpaperToken, toTop = false) + wct.assertWithoutHop(ReorderPredicate(homeTask.token, toTop = null)) } @Test @@ -2114,6 +2197,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { val homeTask = setUpHomeTask() val task = setUpFreeformTask() @@ -2139,6 +2223,61 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Removes wallpaper activity when leaving desktop + wct.assertReorder(wallpaperToken, toTop = false) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -2165,6 +2304,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + // Setup task2 + setUpFreeformTask() + + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) + assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode = + WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val task1Change = assertNotNull(wct.changes[task1.token.asBinder()]) + assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Does not remove wallpaper activity, as desktop still has a visible desktop task + wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false)) + } + + @Test fun moveToFullscreen_nonExistentTask_doesNothing() { controller.moveToFullscreen(999, transitionSource = UNKNOWN) verifyExitDesktopWCTNotExecuted() @@ -2760,22 +2922,97 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowClose_lastWindow_deactivatesDesk() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + + verify(desksOrganizer).deactivateDesk(wct, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowClose_lastWindow_addsPendingDeactivateTransition() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + + val transition = Binder() + val runOnTransitStart = + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + runOnTransitStart(transition) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId = 0)) + } + + @Test fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = false) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) val captor = argumentCaptor<WindowContainerTransaction>() - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(false)) captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowMinimize_lastWindow_deactivatesDesk() { + val task = setUpFreeformTask() + val transition = Binder() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true)) + verify(desksOrganizer).deactivateDesk(captor.firstValue, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowMinimize_lastWindow_addsPendingDeactivateTransition() { + val task = setUpFreeformTask() + val transition = Binder() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0)) + } + + @Test fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { val task = setUpPipTask(autoEnterEnabled = true) val handler = mock(TransitionHandler::class.java) @@ -2785,18 +3022,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) verify(freeformTaskTransitionStarter).startPipTransition(any()) - verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any()) + verify(freeformTaskTransitionStarter, never()) + .startMinimizedModeTransition(any(), anyInt(), anyBoolean()) } @Test fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() { val task = setUpPipTask(autoEnterEnabled = false) - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(Binder()) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(any(), eq(task.taskId), anyBoolean()) verify(freeformTaskTransitionStarter, never()).startPipTransition(any()) } @@ -2820,13 +3065,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = true) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) val captor = argumentCaptor<WindowContainerTransaction>() - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true)) captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } } @@ -2835,14 +3087,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { val task = setUpFreeformTask() val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) // The only active task is being minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) val captor = argumentCaptor<WindowContainerTransaction>() - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true)) // Adds remove wallpaper operation captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @@ -2851,7 +3110,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() { val task = setUpFreeformTask() val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) @@ -2859,7 +3124,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) val captor = argumentCaptor<WindowContainerTransaction>() - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(false)) captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } @@ -2870,13 +3136,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(active = true) setUpFreeformTask(active = true) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) val captor = argumentCaptor<WindowContainerTransaction>() - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task1.taskId), eq(false)) captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } @@ -2888,7 +3161,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(active = true) val task2 = setUpFreeformTask(active = true) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) @@ -2896,7 +3175,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) // Adds remove wallpaper operation val captor = argumentCaptor<WindowContainerTransaction>() - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task1.taskId), eq(true)) // Adds remove wallpaper operation captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @@ -2905,7 +3185,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onDesktopWindowMinimize_triesToExitImmersive() { val task = setUpFreeformTask() val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) @@ -2918,7 +3204,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task = setUpFreeformTask() val transition = Binder() val runOnTransit = RunOnStartTransitionCallback() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any())) .thenReturn( @@ -3972,10 +4264,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) assertThat(taskChange.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wct.assertReorder(wallpaperToken, toTop = false) } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -3999,6 +4292,52 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Does not remove wallpaper activity + wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task2, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() { val freeformTask = setUpFreeformTask() @@ -4420,7 +4759,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) val rectAfterEnd = Rect(100, 50, 500, 1150) verify(transitions) @@ -4458,7 +4796,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -4498,7 +4835,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -4539,7 +4875,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert the task exits desktop mode @@ -4577,7 +4912,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert bounds set to stable bounds @@ -4633,7 +4967,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT @@ -5053,7 +5386,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, - desktopWindowDecoration, ) // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) @@ -5099,7 +5431,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) @@ -5143,7 +5474,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) @@ -5173,7 +5503,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) verify(mReturnToDragStartAnimator) .start( @@ -5198,7 +5527,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT @@ -5225,7 +5553,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE, - desktopWindowDecoration, ) // Assert bounds set to half of the stable bounds @@ -6278,15 +6605,46 @@ private fun WindowContainerTransaction.assertWithoutHop( assertThat(hierarchyOps.none(predicate)).isTrue() } -private fun WindowContainerTransaction.assertReorder( +private fun WindowContainerTransaction.indexOfReorder( task: RunningTaskInfo, toTop: Boolean? = null, -) { - assertHop { hop -> +): Int { + val hop = hierarchyOps.singleOrNull(ReorderPredicate(task.token, toTop)) ?: return -1 + return hierarchyOps.indexOf(hop) +} + +private class ReorderPredicate(val token: WindowContainerToken, val toTop: Boolean? = null) : + ((WindowContainerTransaction.HierarchyOp) -> Boolean) { + override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean = hop.type == HIERARCHY_OP_TYPE_REORDER && (toTop == null || hop.toTop == toTop) && - hop.container == task.token.asBinder() - } + hop.container == token.asBinder() +} + +private class ReparentPredicate( + val token: WindowContainerToken, + val parentToken: WindowContainerToken, + val toTop: Boolean? = null, +) : ((WindowContainerTransaction.HierarchyOp) -> Boolean) { + override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean = + hop.isReparent && + (toTop == null || hop.toTop == toTop) && + hop.container == token.asBinder() && + hop.newParent == parentToken.asBinder() +} + +private fun WindowContainerTransaction.assertReorder( + task: RunningTaskInfo, + toTop: Boolean? = null, +) { + assertReorder(task.token, toTop) +} + +private fun WindowContainerTransaction.assertReorder( + token: WindowContainerToken, + toTop: Boolean? = null, +) { + assertHop(ReorderPredicate(token, toTop)) } private fun WindowContainerTransaction.assertReorderAt( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index ba26d1df94f6..85f6cd36992d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -51,6 +51,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.MockitoSession +import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -180,10 +181,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { defaultHandler, DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_LEFT, ) - verify(bubbleController).expandStackAndSelectBubble( - any<RunningTaskInfo>(), - any<BubbleTransitions.DragData>() - ) + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { isReleasedOnLeft }, + ) } @Test @@ -192,10 +194,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { defaultHandler, DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_RIGHT, ) - verify(bubbleController).expandStackAndSelectBubble( - any<RunningTaskInfo>(), - any<BubbleTransitions.DragData>() - ) + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { !isReleasedOnLeft }, + ) } @Test @@ -382,10 +385,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) // Verify the request went through bubble controller. - verify(bubbleController).expandStackAndSelectBubble( - any<RunningTaskInfo>(), - any<BubbleTransitions.DragData>() - ) + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { isReleasedOnLeft }, + ) } @Test @@ -398,10 +402,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) // Verify the request went through bubble controller. - verify(bubbleController).expandStackAndSelectBubble( - any<RunningTaskInfo>(), - any<BubbleTransitions.DragData>() - ) + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { !isReleasedOnLeft }, + ) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt new file mode 100644 index 000000000000..aa4e9aaf248e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2025 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.desktopwallpaperactivity + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.filters.SmallTest +import com.android.wm.shell.MockToken +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test class for [DesktopWallpaperActivityTokenProvider] + * + * Usage: atest WMShellUnitTests:DesktopWallpaperActivityTokenProviderTest + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DesktopWallpaperActivityTokenProviderTest : ShellTestCase() { + + private lateinit var provider: DesktopWallpaperActivityTokenProvider + private val DEFAULT_DISPLAY = 0 + private val SECONDARY_DISPLAY = 1 + + @Before + fun setUp() { + provider = DesktopWallpaperActivityTokenProvider() + } + + @Test + fun setToken_setsTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token) + } + + @Test + fun setToken_overwritesExistingTokenForDisplay() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.setToken(token2, DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token2) + } + + @Test + fun getToken_returnsNullForNonExistentDisplay() { + assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull() + } + + @Test + fun removeToken_removesTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + provider.removeToken(DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + } + + @Test + fun removeToken_withToken_removesTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + provider.removeToken(token) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + } + + @Test + fun removeToken_doesNothingForNonExistentDisplay() { + provider.removeToken(SECONDARY_DISPLAY) + + assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull() + } + + @Test + fun removeToken_withNonExistentToken_doesNothing() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.removeToken(token2) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1) + } + + @Test + fun multipleDisplays_tokensAreIndependent() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.setToken(token2, SECONDARY_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1) + assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2) + + provider.removeToken(DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt index 9f09e3f57927..4dcf669f4d25 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt @@ -20,6 +20,7 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo @@ -177,4 +178,70 @@ class DesksTransitionObserverTest : ShellTestCase() { assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(deskId) assertThat(repository.getActiveTaskIdsInDesk(deskId)).contains(task.taskId) } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDesk_updatesRepository() { + val transition = Binder() + val deskChange = Change(mock(), mock()) + whenever(mockDesksOrganizer.isDeskChange(deskChange, deskId = 5)).thenReturn(true) + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply { addChange(deskChange) }, + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDeskWithExitingTask_updatesRepository() { + val transition = Binder() + val exitingTask = createFreeformTask(DEFAULT_DISPLAY) + val exitingTaskChange = Change(mock(), mock()).apply { taskInfo = exitingTask } + whenever(mockDesksOrganizer.getDeskAtEnd(exitingTaskChange)).thenReturn(null) + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + repository.addTaskToDesk( + displayId = DEFAULT_DISPLAY, + deskId = 5, + taskId = exitingTask.taskId, + isVisible = true, + ) + assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isTrue() + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = + TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply { + addChange(exitingTaskChange) + }, + ) + + assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDeskWithoutVisibleChange_updatesRepository() { + val transition = Binder() + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0), + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 4d4b15389eca..8b10ca1a2a70 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -23,11 +23,13 @@ import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat @@ -104,54 +106,45 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskVanished_removesRoot() { - val callback = FakeOnCreateCallback() - organizer.createDesk(Display.DEFAULT_DISPLAY, callback) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() - organizer.onTaskVanished(freeformRoot) + organizer.onTaskVanished(desk.taskInfo) - assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse() + assertThat(organizer.roots.contains(desk.deskId)).isFalse() } @Test fun testDesktopWindowAppearsInDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskId } organizer.onTaskAppeared(child, SurfaceControl()) - assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId) + assertThat(desk.children).contains(child.taskId) } @Test fun testDesktopWindowDisappearsFromDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskId } organizer.onTaskAppeared(child, SurfaceControl()) organizer.onTaskVanished(child) - assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId) + assertThat(desk.children).doesNotContain(child.taskId) } @Test fun testRemoveDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val wct = WindowContainerTransaction() - organizer.removeDesk(wct, freeformRoot.taskId) + organizer.removeDesk(wct, desk.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -167,25 +160,23 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testActivateDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val wct = WindowContainerTransaction() - organizer.activateDesk(wct, freeformRoot.taskId) + organizer.activateDesk(wct, desk.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER && hop.toTop && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -201,20 +192,18 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testMoveTaskToDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val desktopTask = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() - organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask) + organizer.moveTaskToDesk(wct, desk.deskId, desktopTask) assertThat( wct.hierarchyOps.any { hop -> hop.isReparent && hop.toTop && hop.container == desktopTask.token.asBinder() && - hop.newParent == freeformRoot.token.asBinder() + hop.newParent == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -240,17 +229,15 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testGetDeskAtEnd() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() - val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val task = createFreeformTask().apply { parentTaskId = desk.deskId } val endDesk = organizer.getDeskAtEnd( TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } ) - assertThat(endDesk).isEqualTo(freeformRoot.taskId) + assertThat(endDesk).isEqualTo(desk.deskId) } @Test @@ -273,6 +260,47 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { assertThat(isActive).isTrue() } + @Test + fun deactivateDesk_clearsLaunchRoot() { + val wct = WindowContainerTransaction() + val desk = createDesk() + organizer.activateDesk(wct, desk.deskId) + + organizer.deactivateDesk(wct, desk.deskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && + hop.container == desk.taskInfo.token.asBinder() && + hop.windowingModes == null && + hop.activityTypes == null + } + ) + .isTrue() + } + + @Test + fun isDeskChange() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply { + taskInfo = desk.taskInfo + }, + desk.deskId, + ) + ) + .isTrue() + } + + private fun createDesk(): DeskRoot { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + return organizer.roots[freeformRoot.taskId] + } + private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { var deskId: Int? = null val created: Boolean diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index 8e0381e4f933..0c1952910d1a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; @@ -26,6 +27,7 @@ import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.times; import static org.mockito.kotlin.VerificationKt.verify; +import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Matrix; @@ -44,15 +46,19 @@ import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; +import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + /** * Unit test against {@link PipScheduler} */ @@ -77,6 +83,8 @@ public class PipSchedulerTest { @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; + @Mock private SplitScreenController mMockSplitScreenController; + @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor; @@ -93,7 +101,8 @@ public class PipSchedulerTest { .thenReturn(mMockTransaction); mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor, - mMockPipTransitionState, mMockPipDesktopState); + mMockPipTransitionState, Optional.of(mMockSplitScreenController), + mMockPipDesktopState); mPipScheduler.setPipTransitionController(mMockPipTransitionController); mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory); mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, startTx, finishTx, direction) -> @@ -119,12 +128,18 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, never()).startExpandTransition(any()); + verify(mMockPipTransitionController, never()).startExpandTransition(any(), anyBoolean()); } @Test - public void scheduleExitPipViaExpand_exitTransitionCalled() { + public void scheduleExitPipViaExpand_noSplit_expandTransitionCalled() { setMockPipTaskToken(); + ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo); + + // Make sure task with the id = 1 isn't in split-screen. + when(mMockSplitScreenController.isTaskInSplitScreen( + ArgumentMatchers.eq(1))).thenReturn(false); mPipScheduler.scheduleExitPipViaExpand(); @@ -132,7 +147,29 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, times(1)).startExpandTransition(any()); + verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean()); + } + + @Test + public void scheduleExitPipViaExpand_lastParentInSplit_prepareSplitAndExpand() { + setMockPipTaskToken(); + ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo); + + // Make sure task with the id = 1 is in split-screen. + when(mMockSplitScreenController.isTaskInSplitScreen( + ArgumentMatchers.eq(1))).thenReturn(true); + + mPipScheduler.scheduleExitPipViaExpand(); + + verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); + assertNotNull(mRunnableArgumentCaptor.getValue()); + mRunnableArgumentCaptor.getValue().run(); + + // We need to both prepare the split screen with the last parent and start expanding. + verify(mMockSplitScreenController, + times(1)).prepareEnterSplitScreen(any(), any(), anyInt()); + verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean()); } @Test @@ -259,4 +296,10 @@ public class PipSchedulerTest { private void setMockPipTaskToken() { when(mMockPipTransitionState.getPipTaskToken()).thenReturn(mMockPipTaskToken); } + + private ActivityManager.RunningTaskInfo getTaskInfoWithLastParentBeforePip(int lastParentId) { + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.lastParentTaskIdBeforePip = lastParentId; + return taskInfo; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java new file mode 100644 index 000000000000..2e389b7dd151 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2017 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.pip2.phone; + +import static android.view.MotionEvent.ACTION_BUTTON_PRESS; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.SystemClock; +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PipTouchStateTest extends ShellTestCase { + + private PipTouchState mTouchState; + private CountDownLatch mDoubleTapCallbackTriggeredLatch; + private CountDownLatch mHoverExitCallbackTriggeredLatch; + private TestShellExecutor mMainExecutor; + + @Before + public void setUp() throws Exception { + mMainExecutor = new TestShellExecutor(); + mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1); + mHoverExitCallbackTriggeredLatch = new CountDownLatch(1); + mTouchState = new PipTouchState(ViewConfiguration.get(getContext()), + mDoubleTapCallbackTriggeredLatch::countDown, + mHoverExitCallbackTriggeredLatch::countDown, + mMainExecutor); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + } + + @Test + public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertTrue(mTouchState.isWaitingForDoubleTap()); + + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10); + mTouchState.scheduleDoubleTapTimeoutCallback(); + + mMainExecutor.flushAll(); + assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testDoubleTapDrag_doubleTapCanceled() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500)); + assertTrue(mTouchState.isDragging()); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTap_doubleTapRegistered() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertTrue(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(), + 0, 0)); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) { + return MotionEvent.obtain(0, eventTime, action, x, y, 0); + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java new file mode 100644 index 000000000000..2a22842eda1a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025 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.pip2.phone.transition; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_CHANGE; + +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.VerificationKt.times; +import static org.mockito.kotlin.VerificationKt.verify; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipTransitionState; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.TransitionInfoBuilder; +import com.android.wm.shell.util.StubTransaction; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * Unit test against {@link PipExpandHandler} + */ + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipExpandHandlerTest { + @Mock private Context mMockContext; + @Mock private PipBoundsState mMockPipBoundsState; + @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PipTransitionState mMockPipTransitionState; + @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; + @Mock private SplitScreenController mMockSplitScreenController; + + @Mock private IBinder mMockTransitionToken; + @Mock private TransitionRequestInfo mMockRequestInfo; + @Mock private StubTransaction mStartT; + @Mock private StubTransaction mFinishT; + @Mock private SurfaceControl mPipLeash; + + @Mock private PipExpandAnimator mMockPipExpandAnimator; + + @Surface.Rotation + private static final int DISPLAY_ROTATION = Surface.ROTATION_0; + + private static final float SNAP_FRACTION = 1.5f; + private static final Rect PIP_BOUNDS = new Rect(0, 0, 100, 100); + private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000); + private static final Rect RIGHT_HALF_DISPLAY_BOUNDS = new Rect(500, 0, 1000, 1000); + + private PipExpandHandler mPipExpandHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockPipBoundsState.getBounds()).thenReturn(PIP_BOUNDS); + when(mMockPipBoundsAlgorithm.getSnapFraction(eq(PIP_BOUNDS))).thenReturn(SNAP_FRACTION); + when(mMockPipDisplayLayoutState.getRotation()).thenReturn(DISPLAY_ROTATION); + + mPipExpandHandler = new PipExpandHandler(mMockContext, mMockPipBoundsState, + mMockPipBoundsAlgorithm, mMockPipTransitionState, mMockPipDisplayLayoutState, + Optional.of(mMockSplitScreenController)); + mPipExpandHandler.setPipExpandAnimatorSupplier((context, leash, startTransaction, + finishTransaction, baseBounds, startBounds, endBounds, + sourceRectHint, rotation) -> mMockPipExpandAnimator); + } + + @Test + public void handleRequest_returnNull() { + // All expand from PiP transitions are started in Shell, so handleRequest shouldn't be + // returning any non-null WCT + WindowContainerTransaction wct = mPipExpandHandler.handleRequest( + mMockTransitionToken, mMockRequestInfo); + assertNull(wct); + } + + @Test + public void startAnimation_transitExit_startExpandAnimator() { + final ActivityManager.RunningTaskInfo pipTaskInfo = createPipTaskInfo( + 1, WINDOWING_MODE_FULLSCREEN, new PictureInPictureParams.Builder().build()); + + final TransitionInfo info = getExpandFromPipTransitionInfo( + TRANSIT_EXIT_PIP, pipTaskInfo, null /* lastParent */, false /* toSplit */); + final WindowContainerToken pipToken = pipTaskInfo.getToken(); + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken); + + mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT, + (wct) -> {}); + + verify(mMockPipExpandAnimator, times(1)).start(); + verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + } + + @Test + public void startAnimation_transitExitToSplit_startExpandAnimator() { + // The task info of the task that was pinned while we were in PiP. + final WindowContainerToken pipToken = createPipTaskInfo(1, WINDOWING_MODE_FULLSCREEN, + new PictureInPictureParams.Builder().build()).getToken(); + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken); + + // Change representing the ActivityRecord we are animating in the multi-activity PiP case; + // make sure change's taskInfo=null as this is an activity, but let lastParent be PiP token. + final TransitionInfo info = getExpandFromPipTransitionInfo( + TRANSIT_EXIT_PIP_TO_SPLIT, null /* taskInfo */, pipToken, true /* toSplit */); + + mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT, + (wct) -> {}); + + verify(mMockSplitScreenController, times(1)).finishEnterSplitScreen(eq(mFinishT)); + verify(mMockPipExpandAnimator, times(1)).start(); + verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + } + + private TransitionInfo getExpandFromPipTransitionInfo(@WindowManager.TransitionType int type, + @Nullable ActivityManager.RunningTaskInfo pipTaskInfo, + @Nullable WindowContainerToken lastParent, boolean toSplit) { + final TransitionInfo info = new TransitionInfoBuilder(type) + .addChange(TRANSIT_CHANGE, pipTaskInfo).build(); + final TransitionInfo.Change pipChange = info.getChanges().getFirst(); + pipChange.setRotation(DISPLAY_ROTATION, + WindowConfiguration.ROTATION_UNDEFINED); + pipChange.setStartAbsBounds(PIP_BOUNDS); + pipChange.setEndAbsBounds(toSplit ? RIGHT_HALF_DISPLAY_BOUNDS : DISPLAY_BOUNDS); + pipChange.setLeash(mPipLeash); + pipChange.setLastParent(lastParent); + return info; + } + + private static ActivityManager.RunningTaskInfo createPipTaskInfo(int taskId, + int windowingMode, PictureInPictureParams params) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + taskInfo.token = mock(WindowContainerToken.class); + taskInfo.baseIntent = mock(Intent.class); + taskInfo.pictureInPictureParams = params; + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt index 391d46287498..4082ffd4ac0a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -22,8 +22,6 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.Presubmit import android.platform.test.flag.junit.SetFlagsRule -import android.provider.Settings -import android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES import android.window.DesktopModeFlags import androidx.test.filters.SmallTest import com.android.internal.R @@ -63,14 +61,12 @@ class DesktopModeStatusTest : ShellTestCase() { doReturn(context.contentResolver).whenever(mockContext).contentResolver resetDesktopModeFlagsCache() resetEnforceDeviceRestriction() - resetFlagOverride() } @After fun tearDown() { resetDesktopModeFlagsCache() resetEnforceDeviceRestriction() - resetFlagOverride() } @DisableFlags( @@ -157,23 +153,40 @@ class DesktopModeStatusTest : ShellTestCase() { } @Test - fun isInternalDisplayEligibleToHostDesktops_configDEModeOn_returnsTrue() { + fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktop_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) - assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + } + + @Test + fun isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() { + doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + } + + @Test + fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() { - assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() { - assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isFalse() + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @@ -183,7 +196,7 @@ class DesktopModeStatusTest : ShellTestCase() { eq(R.bool.config_isDesktopModeDevOptionSupported) ) - assertThat(DesktopModeStatus.isInternalDisplayEligibleToHostDesktops(mockContext)).isTrue() + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() } @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) @@ -229,18 +242,11 @@ class DesktopModeStatusTest : ShellTestCase() { cachedToggleOverride.set(null, null) } - private fun resetFlagOverride() { - Settings.Global.putString( - context.contentResolver, - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null - ) - } - private fun setFlagOverride(override: DesktopModeFlags.ToggleOverride) { - Settings.Global.putInt( - context.contentResolver, - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.setting - ) + val cachedToggleOverride = + DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride") + cachedToggleOverride.isAccessible = true + cachedToggleOverride.set(null, override) } private fun setDeviceEligibleForDesktopMode(eligible: Boolean) { 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 da41a23f066c..d8d45c02b364 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 @@ -484,7 +484,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.LEFT), eq(ResizeTrigger.SNAP_LEFT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor) ) } @@ -520,7 +519,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.LEFT), eq(ResizeTrigger.SNAP_LEFT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -542,7 +540,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -562,7 +559,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.RIGHT), eq(ResizeTrigger.SNAP_RIGHT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -598,7 +594,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.RIGHT), eq(ResizeTrigger.SNAP_RIGHT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -620,7 +615,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index e40034b09f39..8cccdb2b6120 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -81,6 +81,7 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopM import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier +import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder import org.junit.After import org.mockito.Mockito @@ -147,6 +148,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>() protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>() protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>() + protected val mockTilingWindowDecoration = mock<DesktopTilingDecorViewModel>() protected val motionEvent = mock<MotionEvent>() private val displayLayout = mock<DisplayLayout>() private val display = mock<Display>() @@ -226,6 +228,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mock<WindowDecorTaskResourceLoader>(), mockRecentsTransitionHandler, desktopModeCompatPolicy, + mockTilingWindowDecoration, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 71c821dd9b71..c4f70ac2297f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -120,6 +120,7 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.MainCoroutineDispatcher; @@ -998,8 +999,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); assertTrue(decoration.isMaximizeMenuActive()); } @@ -1011,8 +1012,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new FakeMaximizeMenuFactory(menu)); decoration.setAppHeaderMaximizeButtonHovered(false); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(false); @@ -1050,8 +1051,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(true); @@ -1065,8 +1066,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); decoration.setAppHeaderMaximizeButtonHovered(true); @@ -1086,7 +1087,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), /* showImmersiveOption= */ eq(true), anyBoolean(), any(), @@ -1111,7 +1111,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), /* showImmersiveOption= */ eq(false), anyBoolean(), any(), @@ -1136,7 +1135,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), anyBoolean(), /* showSnapOptions= */ eq(true), any(), @@ -1161,7 +1159,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), anyBoolean(), /* showSnapOptions= */ eq(false), any(), @@ -1766,7 +1763,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer, @NonNull DisplayController displayController, @NonNull ActivityManager.RunningTaskInfo taskInfo, - @NonNull Context decorWindowContext, @NonNull PointF menuPosition, + @NonNull Context decorWindowContext, + @NonNull Function2<? super Integer,? super Integer,? extends PointF> + positionSupplier, @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { return mMaximizeMenu; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index aa1f82e3e4d8..af0162334440 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -40,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; @@ -386,6 +387,49 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockWindowDecorViewHost).updateView(same(mMockView), any(), any(), any(), any()); } + + @Test + public void testReinflateViewsOnFontScaleChange() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo2.configuration.fontScale = taskInfo.configuration.fontScale + 1; + windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since the font scale has changed. + verify(windowDecor).releaseViews(any()); + } + + @Test + public void testViewNotReinflatedWhenFontScaleNotChanged() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since task info (and therefore the + // fontScale) has not changed. + verify(windowDecor, never()).releaseViews(any()); + } + @Test public void testAddViewHostViewContainer() { final Display defaultDisplay = mock(Display.class); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt index d99a4825e580..c86730ed1dc7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt @@ -20,6 +20,7 @@ import android.testing.TestableLooper import android.view.SurfaceControl import android.view.View import android.view.WindowManager +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.google.common.truth.Truth.assertThat @@ -30,6 +31,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.verify @@ -47,24 +51,46 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { fun update_differentView_replacesView() = runTest { val view = View(context) val lp = WindowManager.LayoutParams() - val reusableVH = createReusableViewHost() - reusableVH.updateView(view, lp, context.resources.configuration, null) + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView) + reusableVH.updateView(view, lp, context.resources.configuration) - assertThat(reusableVH.rootView.childCount).isEqualTo(1) - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view) + assertThat(rootView.childCount).isEqualTo(1) + assertThat(rootView.getChildAt(0)).isEqualTo(view) val newView = View(context) val newLp = WindowManager.LayoutParams() - reusableVH.updateView(newView, newLp, context.resources.configuration, null) + reusableVH.updateView(newView, newLp, context.resources.configuration) - assertThat(reusableVH.rootView.childCount).isEqualTo(1) - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView) + assertThat(rootView.childCount).isEqualTo(1) + assertThat(rootView.getChildAt(0)).isEqualTo(newView) + } + + @Test + fun update_sameView_doesNotReplaceView() = runTest { + val view = View(context) + val lp = WindowManager.LayoutParams() + val spyRootView = spy(FrameLayout(context)) + val reusableVH = createReusableViewHost(spyRootView) + reusableVH.updateView(view, lp, context.resources.configuration) + + verify(spyRootView, times(1)).removeAllViews() + assertThat(spyRootView.childCount).isEqualTo(1) + assertThat(spyRootView.getChildAt(0)).isEqualTo(view) + + reusableVH.updateView(view, lp, context.resources.configuration) + + clearInvocations(spyRootView) + verify(spyRootView, never()).removeAllViews() + assertThat(spyRootView.childCount).isEqualTo(1) + assertThat(spyRootView.getChildAt(0)).isEqualTo(view) } @OptIn(ExperimentalCoroutinesApi::class) @Test fun updateView_clearsPendingAsyncJob() = runTest { - val reusableVH = createReusableViewHost() + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView) val asyncView = View(context) val syncView = View(context) val asyncAttrs = WindowManager.LayoutParams(100, 100) @@ -83,7 +109,6 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { view = syncView, attrs = syncAttrs, configuration = context.resources.configuration, - onDrawTransaction = null, ) // Would run coroutine if it hadn't been cancelled. @@ -91,7 +116,7 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() // View host view/attrs should match the ones from the sync call. - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView) + assertThat(rootView.getChildAt(0)).isEqualTo(syncView) assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width) } @@ -118,7 +143,8 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { @OptIn(ExperimentalCoroutinesApi::class) @Test fun updateViewAsync_clearsPendingAsyncJob() = runTest { - val reusableVH = createReusableViewHost() + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView) val view = View(context) reusableVH.updateViewAsync( @@ -136,7 +162,7 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { advanceUntilIdle() assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView) + assertThat(rootView.getChildAt(0)).isEqualTo(otherView) } @Test @@ -148,7 +174,6 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { view = view, attrs = WindowManager.LayoutParams(100, 100), configuration = context.resources.configuration, - onDrawTransaction = null, ) val t = mock(SurfaceControl.Transaction::class.java) @@ -159,19 +184,23 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { @Test fun warmUp_addsRootView() = runTest { - val reusableVH = createReusableViewHost().apply { warmUp() } + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView).apply { warmUp() } assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView) + assertThat(reusableVH.view()).isEqualTo(rootView) } - private fun CoroutineScope.createReusableViewHost() = + private fun CoroutineScope.createReusableViewHost( + rootView: FrameLayout = FrameLayout(context) + ) = ReusableWindowDecorViewHost( context = context, mainScope = this, display = context.display, id = 1, viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)), + rootView ) private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index dbb891455ddd..e693fcfd3918 100644 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -162,10 +162,13 @@ const std::string& ApkAssets::GetDebugName() const { return assets_provider_->GetDebugName(); } -bool ApkAssets::IsUpToDate() const { +UpToDate ApkAssets::IsUpToDate() const { // Loaders are invalidated by the app, not the system, so assume they are up to date. - return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate()) - && assets_provider_->IsUpToDate()); + if (IsLoader()) { + return UpToDate::Always; + } + const auto idmap_res = loaded_idmap_ ? loaded_idmap_->IsUpToDate() : UpToDate::Always; + return combine(idmap_res, [this] { return assets_provider_->IsUpToDate(); }); } } // namespace android diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 0fa31c7a832e..f5e10d94452f 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -1467,8 +1467,6 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( } const StringPiece16 kAttr16 = u"attr"; - const static std::u16string kAttrPrivate16 = u"^attr-private"; - for (const PackageGroup& package_group : package_groups_) { for (const ConfiguredPackage& package_impl : package_group.packages_) { const LoadedPackage* package = package_impl.loaded_package_; @@ -1480,12 +1478,13 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( base::expected<uint32_t, NullOrIOError> resid = package->FindEntryByName(type16, entry16); if (UNLIKELY(IsIOError(resid))) { return base::unexpected(resid.error()); - } + } if (!resid.has_value() && kAttr16 == type16) { // Private attributes in libraries (such as the framework) are sometimes encoded // under the type '^attr-private' in order to leave the ID space of public 'attr' // free for future additions. Check '^attr-private' for the same name. + const static std::u16string kAttrPrivate16 = u"^attr-private"; resid = package->FindEntryByName(kAttrPrivate16, entry16); } diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp index 2d3c06506a1f..808509120462 100644 --- a/libs/androidfw/AssetsProvider.cpp +++ b/libs/androidfw/AssetsProvider.cpp @@ -24,9 +24,27 @@ #include <ziparchive/zip_archive.h> namespace android { -namespace { -constexpr const char* kEmptyDebugString = "<empty>"; -} // namespace + +static constexpr std::string_view kEmptyDebugString = "<empty>"; + +std::unique_ptr<AssetsProvider> AssetsProvider::CreateWithOverride( + std::unique_ptr<AssetsProvider> provider, std::unique_ptr<AssetsProvider> override) { + if (provider == nullptr) { + return {}; + } + if (override == nullptr) { + return provider; + } + return MultiAssetsProvider::Create(std::move(override), std::move(provider)); +} + +std::unique_ptr<AssetsProvider> AssetsProvider::CreateFromNullable( + std::unique_ptr<AssetsProvider> nullable) { + if (nullable) { + return nullable; + } + return EmptyAssetsProvider::Create(); +} std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode, bool* file_exists) const { @@ -86,11 +104,9 @@ void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const { } ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path, - package_property_t flags, time_t last_mod_time) - : zip_handle_(handle), - name_(std::move(path)), - flags_(flags), - last_mod_time_(last_mod_time) {} + package_property_t flags, ModDate last_mod_time) + : zip_handle_(handle), name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) { +} std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, package_property_t flags, @@ -104,10 +120,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, return {}; } - struct stat sb{.st_mtime = -1}; + ModDate mod_date = kInvalidModDate; // Skip all up-to-date checks if the file won't ever change. - if (!isReadonlyFilesystem(path.c_str())) { - if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) { + if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) { + if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -116,7 +132,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, } return std::unique_ptr<ZipAssetsProvider>( - new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime)); + new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date)); } std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, @@ -137,10 +153,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, return {}; } - struct stat sb{.st_mtime = -1}; + ModDate mod_date = kInvalidModDate; // Skip all up-to-date checks if the file won't ever change. if (!isReadonlyFilesystem(released_fd)) { - if (fstat(released_fd, &sb) < 0) { + if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -150,7 +166,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, } return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider( - handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime)); + handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date)); } std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, @@ -282,21 +298,16 @@ const std::string& ZipAssetsProvider::GetDebugName() const { return name_.GetDebugName(); } -bool ZipAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == -1) { - return true; +UpToDate ZipAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; } - struct stat sb{}; - if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { - // If fstat fails on the zip archive, return true so the zip archive the resource system does - // attempt to refresh the ApkAsset. - return true; - } - return last_mod_time_ == sb.st_mtime; + return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get()))); } -DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) - : dir_(std::move(path)), last_mod_time_(last_mod_time) {} +DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time) + : dir_(std::move(path)), last_mod_time_(last_mod_time) { +} std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { struct stat sb; @@ -317,7 +328,7 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st const bool isReadonly = isReadonlyFilesystem(path.c_str()); return std::unique_ptr<DirectoryAssetsProvider>( - new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime)); + new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb))); } std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, @@ -346,17 +357,11 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const { return dir_; } -bool DirectoryAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == -1) { - return true; +UpToDate DirectoryAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; } - struct stat sb; - if (stat(dir_.c_str(), &sb) < 0) { - // If stat fails on the zip archive, return true so the zip archive the resource system does - // attempt to refresh the ApkAsset. - return true; - } - return last_mod_time_ == sb.st_mtime; + return fromBool(last_mod_time_ == getFileModDate(dir_.c_str())); } MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, @@ -397,8 +402,8 @@ const std::string& MultiAssetsProvider::GetDebugName() const { return debug_name_; } -bool MultiAssetsProvider::IsUpToDate() const { - return primary_->IsUpToDate() && secondary_->IsUpToDate(); +UpToDate MultiAssetsProvider::IsUpToDate() const { + return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); }); } EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) : @@ -438,12 +443,12 @@ const std::string& EmptyAssetsProvider::GetDebugName() const { if (path_.has_value()) { return *path_; } - const static std::string kEmpty = kEmptyDebugString; + constexpr static std::string kEmpty{kEmptyDebugString}; return kEmpty; } -bool EmptyAssetsProvider::IsUpToDate() const { - return true; +UpToDate EmptyAssetsProvider::IsUpToDate() const { + return UpToDate::Always; } } // namespace android diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index 095be57a5dc8..f0ef97e5bdcc 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -22,9 +22,10 @@ #include "android-base/logging.h" #include "android-base/stringprintf.h" #include "android-base/utf8.h" -#include "androidfw/misc.h" +#include "androidfw/AssetManager.h" #include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" +#include "androidfw/misc.h" #include "utils/ByteOrder.h" #include "utils/Trace.h" @@ -280,11 +281,16 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head configurations_(configs), overlay_entries_(overlay_entries), string_pool_(std::move(string_pool)), - idmap_fd_( - android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)), overlay_apk_path_(overlay_apk_path), target_apk_path_(target_apk_path), - idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) { + idmap_last_mod_time_(kInvalidModDate) { + if (!isReadonlyFilesystem(std::string(overlay_apk_path_).c_str()) || + !(target_apk_path_ == AssetManager::TARGET_APK_PATH || + isReadonlyFilesystem(std::string(target_apk_path_).c_str()))) { + idmap_fd_.reset( + android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)); + idmap_last_mod_time_ = getFileModDate(idmap_fd_); + } } std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) { @@ -405,8 +411,11 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie std::move(idmap_string_pool),*overlay_path, *target_path)); } -bool LoadedIdmap::IsUpToDate() const { - return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()); +UpToDate LoadedIdmap::IsUpToDate() const { + if (idmap_last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; + } + return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get())); } } // namespace android diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp index ea9e9a2d4280..9aacdcb9ca92 100644 --- a/libs/androidfw/LocaleDataLookup.cpp +++ b/libs/androidfw/LocaleDataLookup.cpp @@ -14871,12 +14871,22 @@ static uint32_t findLatnParent(uint32_t packed_lang_region) { case 0x656E4154u: // en-AT -> en-150 case 0x656E4245u: // en-BE -> en-150 case 0x656E4348u: // en-CH -> en-150 + case 0x656E435Au: // en-CZ -> en-150 case 0x656E4445u: // en-DE -> en-150 case 0x656E444Bu: // en-DK -> en-150 + case 0x656E4553u: // en-ES -> en-150 case 0x656E4649u: // en-FI -> en-150 + case 0x656E4652u: // en-FR -> en-150 + case 0x656E4855u: // en-HU -> en-150 + case 0x656E4954u: // en-IT -> en-150 case 0x656E4E4Cu: // en-NL -> en-150 + case 0x656E4E4Fu: // en-NO -> en-150 + case 0x656E504Cu: // en-PL -> en-150 + case 0x656E5054u: // en-PT -> en-150 + case 0x656E524Fu: // en-RO -> en-150 case 0x656E5345u: // en-SE -> en-150 case 0x656E5349u: // en-SI -> en-150 + case 0x656E534Bu: // en-SK -> en-150 return 0x656E80A1u; case 0x65734152u: // es-AR -> es-419 case 0x6573424Fu: // es-BO -> es-419 diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 978bc768cd3d..a18c5f5f92f6 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -152,12 +152,11 @@ static void fill9patchOffsets(Res_png_9patch* patch) { patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t)); } -void Res_value::copyFrom_dtoh(const Res_value& src) -{ - size = dtohs(src.size); - res0 = src.res0; - dataType = src.dataType; - data = dtohl(src.data); +void Res_value::copyFrom_dtoh_slow(const Res_value& src) { + size = dtohs(src.size); + res0 = src.res0; + dataType = src.dataType; + data = dtohl(src.data); } void Res_png_9patch::deviceToFile() @@ -2035,16 +2034,6 @@ status_t ResXMLTree::validateNode(const ResXMLTree_node* node) const // -------------------------------------------------------------------- // -------------------------------------------------------------------- -void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) { - const size_t size = dtohl(o.size); - if (size >= sizeof(ResTable_config)) { - *this = o; - } else { - memcpy(this, &o, size); - memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size); - } -} - /* static */ size_t unpackLanguageOrRegion(const char in[2], const char base, char out[4]) { if (in[0] & 0x80) { @@ -2109,34 +2098,33 @@ size_t ResTable_config::unpackRegion(char region[4]) const { return unpackLanguageOrRegion(this->country, '0', region); } - -void ResTable_config::copyFromDtoH(const ResTable_config& o) { - copyFromDeviceNoSwap(o); - size = sizeof(ResTable_config); - mcc = dtohs(mcc); - mnc = dtohs(mnc); - density = dtohs(density); - screenWidth = dtohs(screenWidth); - screenHeight = dtohs(screenHeight); - sdkVersion = dtohs(sdkVersion); - minorVersion = dtohs(minorVersion); - smallestScreenWidthDp = dtohs(smallestScreenWidthDp); - screenWidthDp = dtohs(screenWidthDp); - screenHeightDp = dtohs(screenHeightDp); -} - -void ResTable_config::swapHtoD() { - size = htodl(size); - mcc = htods(mcc); - mnc = htods(mnc); - density = htods(density); - screenWidth = htods(screenWidth); - screenHeight = htods(screenHeight); - sdkVersion = htods(sdkVersion); - minorVersion = htods(minorVersion); - smallestScreenWidthDp = htods(smallestScreenWidthDp); - screenWidthDp = htods(screenWidthDp); - screenHeightDp = htods(screenHeightDp); +void ResTable_config::copyFromDtoH_slow(const ResTable_config& o) { + copyFromDeviceNoSwap(o); + size = sizeof(ResTable_config); + mcc = dtohs(mcc); + mnc = dtohs(mnc); + density = dtohs(density); + screenWidth = dtohs(screenWidth); + screenHeight = dtohs(screenHeight); + sdkVersion = dtohs(sdkVersion); + minorVersion = dtohs(minorVersion); + smallestScreenWidthDp = dtohs(smallestScreenWidthDp); + screenWidthDp = dtohs(screenWidthDp); + screenHeightDp = dtohs(screenHeightDp); +} + +void ResTable_config::swapHtoD_slow() { + size = htodl(size); + mcc = htods(mcc); + mnc = htods(mnc); + density = htods(density); + screenWidth = htods(screenWidth); + screenHeight = htods(screenHeight); + sdkVersion = htods(sdkVersion); + minorVersion = htods(minorVersion); + smallestScreenWidthDp = htods(smallestScreenWidthDp); + screenWidthDp = htods(screenWidthDp); + screenHeightDp = htods(screenHeightDp); } /* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) { @@ -2149,7 +2137,7 @@ void ResTable_config::swapHtoD() { // systems should happen very infrequently (if at all.) // The comparison code relies on memcmp low-level optimizations that make it // more efficient than strncmp. - const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; + static constexpr char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; const char *lScript = l.localeScriptWasComputed ? emptyScript : l.localeScript; const char *rScript = r.localeScriptWasComputed ? emptyScript : r.localeScript; diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp index be55fe8b4bb6..86c459fb4647 100644 --- a/libs/androidfw/Util.cpp +++ b/libs/androidfw/Util.cpp @@ -32,13 +32,18 @@ namespace android { namespace util { void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) { - char buf[5]; - while (*src && len != 0) { - char16_t c = static_cast<char16_t>(dtohs(*src)); - utf16_to_utf8(&c, 1, buf, sizeof(buf)); - out->append(buf, strlen(buf)); - ++src; - --len; + static constexpr bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; + if constexpr (kDeviceEndiannessSame) { + *out = Utf16ToUtf8({(const char16_t*)src, strnlen16((const char16_t*)src, len)}); + } else { + char buf[5]; + while (*src && len != 0) { + char16_t c = static_cast<char16_t>(dtohs(*src)); + utf16_to_utf8(&c, 1, buf, sizeof(buf)); + out->append(buf, strlen(buf)); + ++src; + --len; + } } } @@ -63,8 +68,10 @@ std::string Utf16ToUtf8(StringPiece16 utf16) { } std::string utf8; - utf8.resize(utf8_length); - utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1); + utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) { + utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1); + return size; + }); return utf8; } diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index 231808beb718..3f6f4661f2f7 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -116,7 +116,7 @@ class ApkAssets : public RefBase { return resources_asset_ != nullptr && resources_asset_->isAllocated(); } - bool IsUpToDate() const; + UpToDate IsUpToDate() const; // DANGER! // This is a destructive method that rips the assets provider out of ApkAssets object. diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h index d33c325ff369..037f684f5b78 100644 --- a/libs/androidfw/include/androidfw/AssetsProvider.h +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef ANDROIDFW_ASSETSPROVIDER_H -#define ANDROIDFW_ASSETSPROVIDER_H +#pragma once #include <memory> #include <string> @@ -37,6 +36,12 @@ namespace android { struct AssetsProvider { static constexpr off64_t kUnknownLength = -1; + static std::unique_ptr<AssetsProvider> CreateWithOverride( + std::unique_ptr<AssetsProvider> provider, std::unique_ptr<AssetsProvider> override); + + static std::unique_ptr<AssetsProvider> CreateFromNullable( + std::unique_ptr<AssetsProvider> nullable); + // Opens a file for reading. If `file_exists` is not null, it will be set to `true` if the file // exists. This is useful for determining if the file exists but was unable to be opened due to // an I/O error. @@ -58,7 +63,7 @@ struct AssetsProvider { WARN_UNUSED virtual const std::string& GetDebugName() const = 0; // Returns whether the interface provides the most recent version of its files. - WARN_UNUSED virtual bool IsUpToDate() const = 0; + WARN_UNUSED virtual UpToDate IsUpToDate() const = 0; // Creates an Asset from a file on disk. static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); @@ -95,7 +100,7 @@ struct ZipAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const; ~ZipAssetsProvider() override = default; @@ -106,7 +111,7 @@ struct ZipAssetsProvider : public AssetsProvider { private: struct PathOrDebugName; ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags, - time_t last_mod_time); + ModDate last_mod_time); struct PathOrDebugName { static PathOrDebugName Path(std::string value) { @@ -135,7 +140,7 @@ struct ZipAssetsProvider : public AssetsProvider { std::unique_ptr<ZipArchive, ZipCloser> zip_handle_; PathOrDebugName name_; package_property_t flags_; - time_t last_mod_time_; + ModDate last_mod_time_; }; // Supplies assets from a root directory. @@ -147,7 +152,7 @@ struct DirectoryAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~DirectoryAssetsProvider() override = default; protected: @@ -156,9 +161,9 @@ struct DirectoryAssetsProvider : public AssetsProvider { bool* file_exists) const override; private: - explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time); + explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time); std::string dir_; - time_t last_mod_time_; + ModDate last_mod_time_; }; // Supplies assets from a `primary` asset provider and falls back to supplying assets from the @@ -172,7 +177,7 @@ struct MultiAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~MultiAssetsProvider() override = default; protected: @@ -199,7 +204,7 @@ struct EmptyAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~EmptyAssetsProvider() override = default; protected: @@ -212,5 +217,3 @@ struct EmptyAssetsProvider : public AssetsProvider { }; } // namespace android - -#endif /* ANDROIDFW_ASSETSPROVIDER_H */ diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index d1db13f53069..0c0856315d8f 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef IDMAP_H_ -#define IDMAP_H_ +#pragma once #include <memory> #include <string> @@ -32,6 +31,31 @@ namespace android { +// An enum that tracks more states than just 'up to date' or 'not' for a resources container: +// there are several cases where we know for sure that the object can't change and won't get +// out of date. Reporting those states to the managed layer allows it to stop checking here +// completely, speeding up the cache lookups by dozens of milliseconds. +enum class UpToDate : int { False, True, Always }; + +// Combines two UpToDate values, and only accesses the second one if it matters to the result. +template <class Getter> +UpToDate combine(UpToDate first, Getter secondGetter) { + switch (first) { + case UpToDate::False: + return UpToDate::False; + case UpToDate::True: { + const auto second = secondGetter(); + return second == UpToDate::False ? UpToDate::False : UpToDate::True; + } + case UpToDate::Always: + return secondGetter(); + } +} + +inline UpToDate fromBool(bool value) { + return value ? UpToDate::True : UpToDate::False; +} + class LoadedIdmap; class IdmapResMap; struct Idmap_header; @@ -197,7 +221,7 @@ class LoadedIdmap { // Returns whether the idmap file on disk has not been modified since the construction of this // LoadedIdmap. - bool IsUpToDate() const; + UpToDate IsUpToDate() const; protected: // Exposed as protected so that tests can subclass and mock this class out. @@ -237,5 +261,3 @@ class LoadedIdmap { }; } // namespace android - -#endif // IDMAP_H_ diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 8b2871c21a1e..30594dcfa939 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -47,6 +47,8 @@ namespace android { +constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; + constexpr const uint32_t kIdmapMagic = 0x504D4449u; constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Bu; @@ -408,7 +410,16 @@ struct Res_value typedef uint32_t data_type; data_type data; - void copyFrom_dtoh(const Res_value& src); + void copyFrom_dtoh(const Res_value& src) { + if constexpr (kDeviceEndiannessSame) { + *this = src; + } else { + copyFrom_dtoh_slow(src); + } + } + + private: + void copyFrom_dtoh_slow(const Res_value& src); }; /** @@ -1254,11 +1265,32 @@ struct ResTable_config // Varies in length from 3 to 8 chars. Zero-filled value. char localeNumberingSystem[8]; - void copyFromDeviceNoSwap(const ResTable_config& o); - - void copyFromDtoH(const ResTable_config& o); - - void swapHtoD(); + void copyFromDeviceNoSwap(const ResTable_config& o) { + const auto o_size = dtohl(o.size); + if (o_size >= sizeof(ResTable_config)) [[likely]] { + *this = o; + } else { + memcpy(this, &o, o_size); + memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size); + } + this->size = sizeof(*this); + } + + void copyFromDtoH(const ResTable_config& o) { + if constexpr (kDeviceEndiannessSame) { + copyFromDeviceNoSwap(o); + } else { + copyFromDtoH_slow(o); + } + } + + void swapHtoD() { + if constexpr (kDeviceEndiannessSame) { + ; // noop + } else { + swapHtoD_slow(); + } + } int compare(const ResTable_config& o) const; int compareLogical(const ResTable_config& o) const; @@ -1384,6 +1416,10 @@ struct ResTable_config bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const; String8 toString() const; + + private: + void copyFromDtoH_slow(const ResTable_config& o); + void swapHtoD_slow(); }; /** diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index c9ba8a01a5e9..d8ca64a174a2 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -15,6 +15,7 @@ */ #pragma once +#include <sys/stat.h> #include <time.h> // @@ -64,10 +65,15 @@ ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ ModDate getFileModDate(int fd); +// Extract the modification date from the stat structure. +ModDate getModDate(const struct ::stat& st); + // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); +bool isKnownWritablePath(const char* path); + } // namespace android // Whoever uses getFileModDate() will need this as well diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 32f3624a3aee..26eb320805c9 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -16,10 +16,10 @@ #define LOG_TAG "misc" -// -// Miscellaneous utility functions. -// -#include <androidfw/misc.h> +#include "androidfw/misc.h" + +#include <errno.h> +#include <sys/stat.h> #include "android-base/logging.h" @@ -28,9 +28,7 @@ #include <sys/vfs.h> #endif // __linux__ -#include <errno.h> -#include <sys/stat.h> - +#include <array> #include <cstdio> #include <cstring> #include <tuple> @@ -40,28 +38,26 @@ namespace android { /* * Get a file's type. */ -FileType getFileType(const char* fileName) -{ - struct stat sb; - - if (stat(fileName, &sb) < 0) { - if (errno == ENOENT || errno == ENOTDIR) - return kFileTypeNonexistent; - else { - PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; - return kFileTypeUnknown; - } - } else { - if (S_ISREG(sb.st_mode)) - return kFileTypeRegular; - else if (S_ISDIR(sb.st_mode)) - return kFileTypeDirectory; - else if (S_ISCHR(sb.st_mode)) - return kFileTypeCharDev; - else if (S_ISBLK(sb.st_mode)) - return kFileTypeBlockDev; - else if (S_ISFIFO(sb.st_mode)) - return kFileTypeFifo; +FileType getFileType(const char* fileName) { + struct stat sb; + if (stat(fileName, &sb) < 0) { + if (errno == ENOENT || errno == ENOTDIR) + return kFileTypeNonexistent; + else { + PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; + return kFileTypeUnknown; + } + } else { + if (S_ISREG(sb.st_mode)) + return kFileTypeRegular; + else if (S_ISDIR(sb.st_mode)) + return kFileTypeDirectory; + else if (S_ISCHR(sb.st_mode)) + return kFileTypeCharDev; + else if (S_ISBLK(sb.st_mode)) + return kFileTypeBlockDev; + else if (S_ISFIFO(sb.st_mode)) + return kFileTypeFifo; #if defined(S_ISLNK) else if (S_ISLNK(sb.st_mode)) return kFileTypeSymlink; @@ -75,7 +71,7 @@ FileType getFileType(const char* fileName) } } -static ModDate getModDate(const struct stat& st) { +ModDate getModDate(const struct stat& st) { #ifdef _WIN32 return st.st_mtime; #elif defined(__APPLE__) @@ -113,8 +109,14 @@ bool isReadonlyFilesystem(const char*) { bool isReadonlyFilesystem(int) { return false; } +bool isKnownWritablePath(const char*) { + return false; +} #else // __linux__ bool isReadonlyFilesystem(const char* path) { + if (isKnownWritablePath(path)) { + return false; + } struct statfs sfs; if (::statfs(path, &sfs)) { PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed"; @@ -131,6 +133,13 @@ bool isReadonlyFilesystem(int fd) { } return (sfs.f_flags & ST_RDONLY) != 0; } + +bool isKnownWritablePath(const char* path) { + // We know that all paths in /data/ are writable. + static constexpr char kRwPrefix[] = "/data/"; + return strncmp(kRwPrefix, path, std::size(kRwPrefix) - 1) == 0; +} + #endif // __linux__ } // namespace android diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index cb2e56f5f5e4..22b9e69500d9 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -218,10 +218,11 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { auto apk_assets = ApkAssets::LoadOverlay(temp_file.path); ASSERT_NE(nullptr, apk_assets); - ASSERT_TRUE(apk_assets->IsUpToDate()); + ASSERT_TRUE(apk_assets->IsOverlay()); + ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate()); unlink(temp_file.path); - ASSERT_FALSE(apk_assets->IsUpToDate()); + ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); const auto sleep_duration = std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); @@ -230,7 +231,27 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { base::WriteStringToFile("hello", temp_file.path); std::this_thread::sleep_for(sleep_duration); - ASSERT_FALSE(apk_assets->IsUpToDate()); + ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); +} + +TEST(IdmapTestUpToDate, Combine) { + ASSERT_EQ(UpToDate::False, combine(UpToDate::False, [] { + ADD_FAILURE(); // Shouldn't get called at all. + return UpToDate::False; + })); + + ASSERT_EQ(UpToDate::False, combine(UpToDate::True, [] { return UpToDate::False; })); + + ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::True; })); + ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::Always; })); + ASSERT_EQ(UpToDate::True, combine(UpToDate::Always, [] { return UpToDate::True; })); + + ASSERT_EQ(UpToDate::Always, combine(UpToDate::Always, [] { return UpToDate::Always; })); +} + +TEST(IdmapTestUpToDate, FromBool) { + ASSERT_EQ(UpToDate::False, fromBool(false)); + ASSERT_EQ(UpToDate::True, fromBool(true)); } } // namespace diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 62fd7d358123..d3fc91b65829 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -137,6 +137,14 @@ flag { } flag { + name: "shader_color_space" + is_exported: true + namespace: "core_graphics" + description: "API to set the working colorspace of a Shader or ColorFilter" + bug: "299670828" +} + +flag { name: "query_global_priority" namespace: "core_graphics" description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation." @@ -174,7 +182,7 @@ flag { flag { name: "early_preload_gl_context" namespace: "core_graphics" - description: "Initialize GL context and GraphicBufferAllocater init on renderThread preload. This improves app startup time for apps using GL." + description: "Preload GL context on renderThread preload. This improves app startup time for apps using GL." bug: "383612849" } @@ -187,4 +195,12 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "early_preinit_buffer_allocator" + namespace: "core_graphics" + description: "Initialize GraphicBufferAllocater on ViewRootImpl init, to avoid blocking on init during buffer allocation, improving app launch latency." + bug: "389908734" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index eadb9dea566f..45f0fe0288a4 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -266,11 +266,17 @@ static jlong RuntimeShader_getNativeFinalizer(JNIEnv*, jobject) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeShaderBuilder_delete)); } -static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr) { +static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr, + jlong colorSpacePtr) { SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); + auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr); sk_sp<SkShader> shader = builder->makeShader(matrix); ThrowIAE_IfNull(env, shader); + if (colorSpace) { + shader = shader->makeWithWorkingColorSpace(colorSpace); + ThrowIAE_IfNull(env, shader); + } return reinterpret_cast<jlong>(shader.release()); } @@ -350,6 +356,10 @@ static void RuntimeShader_updateChild(JNIEnv* env, jobject, jlong shaderBuilder, UpdateChild(env, builder, name.c_str(), childEffect); } +static void RuntimeShader_no(JNIEnv* env) { + jniThrowRuntimeException(env, "Not supported"); +} + /////////////////////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gShaderMethods[] = { @@ -379,7 +389,8 @@ static const JNINativeMethod gComposeShaderMethods[] = { static const JNINativeMethod gRuntimeShaderMethods[] = { {"nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer}, - {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_create}, + {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_no}, + {"nativeCreateShader", "(JJJ)J", (void*)RuntimeShader_create}, {"nativeCreateBuilder", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderBuilder}, {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", (void*)RuntimeShader_updateFloatArrayUniforms}, diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index df9f83036709..99e7740d66d2 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -52,6 +52,9 @@ #include <renderthread/RenderThread.h> #include <src/image/SkImage_Base.h> #include <thread/CommonPool.h> +#ifdef __ANDROID__ +#include <ui/GraphicBufferAllocator.h> +#endif #include <utils/Color.h> #include <utils/RefBase.h> #include <utils/StrongPointer.h> @@ -849,6 +852,17 @@ static void android_view_ThreadedRenderer_preload(JNIEnv*, jclass) { RenderProxy::preload(); } +static void android_view_ThreadedRenderer_preInitBufferAllocator(JNIEnv*, jclass) { +#ifdef __ANDROID__ + CommonPool::async([] { + ATRACE_NAME("preInitBufferAllocator:GraphicBufferAllocator"); + // This involves several binder calls which we do not want blocking + // critical path of the activity that is launching. + GraphicBufferAllocator::getInstance(); + }); +#endif +} + static void android_view_ThreadedRenderer_setRtAnimationsEnabled(JNIEnv* env, jobject clazz, jboolean enabled) { RenderProxy::setRtAnimationsEnabled(enabled); @@ -1040,6 +1054,8 @@ static const JNINativeMethod gMethods[] = { (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, {"nInitDisplayInfo", "(IIFIJJZZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, + {"preInitBufferAllocator", "()V", + (void*)android_view_ThreadedRenderer_preInitBufferAllocator}, {"isWebViewOverlaysEnabled", "()Z", (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled}, {"nSetDrawingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDrawingEnabled}, diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java index e1fbfea19235..892a8612d74a 100644 --- a/media/java/android/media/AudioDeviceVolumeManager.java +++ b/media/java/android/media/AudioDeviceVolumeManager.java @@ -86,10 +86,10 @@ public class AudioDeviceVolumeManager { /** * @hide * Interface to receive volume changes on a device that behaves in absolute volume mode. - * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, Executor, - * OnAudioDeviceVolumeChangeListener) - * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, Executor, - * OnAudioDeviceVolumeChangeListener) + * @see #setDeviceAbsoluteMultiVolumeBehavior(AudioDeviceAttributes, List, boolean, Executor, + * OnAudioDeviceVolumeChangedListener) + * @see #setDeviceAbsoluteVolumeBehavior(AudioDeviceAttributes, VolumeInfo, boolean, Executor, + * OnAudioDeviceVolumeChangedListener) */ public interface OnAudioDeviceVolumeChangedListener { /** @@ -203,6 +203,9 @@ public class AudioDeviceVolumeManager { * volume updates to apply on that device * @param device the audio device set to absolute volume mode * @param volume the type of volume this device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @@ -211,13 +214,13 @@ public class AudioDeviceVolumeManager { public void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { final ArrayList<VolumeInfo> volumes = new ArrayList<>(1); volumes.add(volume); - setDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, - handlesVolumeAdjustment); + setDeviceAbsoluteMultiVolumeBehavior(device, volumes, handlesVolumeAdjustment, executor, + vclistener); } /** @@ -226,20 +229,20 @@ public class AudioDeviceVolumeManager { * registers a listener for receiving volume updates to apply on that device * @param device the audio device set to absolute multi-volume mode * @param volumes the list of volumes the given device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates - * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately - * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} - * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. */ @RequiresPermission(anyOf = { android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.BLUETOOTH_PRIVILEGED }) public void setDeviceAbsoluteMultiVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull List<VolumeInfo> volumes, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } @@ -249,11 +252,14 @@ public class AudioDeviceVolumeManager { * Configures a device to use absolute volume model, and registers a listener for receiving * volume updates to apply on that device. * - * Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no reliable - * way to set the device's volume to a percentage. + * <p>Should be used instead of {@link #setDeviceAbsoluteVolumeBehavior} when there is no + * reliable way to set the device's volume to a percentage. * * @param device the audio device set to absolute volume mode * @param volume the type of volume this device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @@ -262,13 +268,13 @@ public class AudioDeviceVolumeManager { public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { final ArrayList<VolumeInfo> volumes = new ArrayList<>(1); volumes.add(volume); - setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, executor, vclistener, - handlesVolumeAdjustment); + setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior(device, volumes, handlesVolumeAdjustment, + executor, vclistener); } /** @@ -276,11 +282,14 @@ public class AudioDeviceVolumeManager { * Configures a device to use absolute volume model applied to different volume types, and * registers a listener for receiving volume updates to apply on that device. * - * Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is + * <p>Should be used instead of {@link #setDeviceAbsoluteMultiVolumeBehavior} when there is * no reliable way to set the device's volume to a percentage. * * @param device the audio device set to absolute multi-volume mode * @param volumes the list of volumes the given device responds to + * @param handlesVolumeAdjustment whether the controller handles volume adjustments separately + * from volume changes. If true, adjustments from {@link AudioManager#adjustStreamVolume} + * will be sent via {@link OnAudioDeviceVolumeChangedListener#onAudioDeviceVolumeAdjusted}. * @param executor the Executor used for receiving volume updates through the listener * @param vclistener the callback for volume updates */ @@ -289,16 +298,16 @@ public class AudioDeviceVolumeManager { public void setDeviceAbsoluteMultiVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull List<VolumeInfo> volumes, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { baseSetDeviceAbsoluteMultiVolumeBehavior(device, volumes, executor, vclistener, handlesVolumeAdjustment, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); } /** * Base method for configuring a device to use absolute volume behavior, or one of its variants. - * See {@link AudioManager#AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors. + * See {@link AudioManager.AbsoluteDeviceVolumeBehavior} for a list of allowed behaviors. * * @param behavior the variant of absolute device volume behavior to adopt */ diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 12d7f33a0d51..e01cb928e369 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -1754,13 +1754,21 @@ public class AudioSystem @UnsupportedAppUsage public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, int codecFormat) { + return setDeviceConnectionState(attributes, state, codecFormat, false /*deviceSwitch*/); + } + + /** + * @hide + */ + public static int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, + int codecFormat, boolean deviceSwitch) { android.media.audio.common.AudioPort port = AidlConversion.api2aidl_AudioDeviceAttributes_AudioPort(attributes); Parcel parcel = Parcel.obtain(); port.writeToParcel(parcel, 0); parcel.setDataPosition(0); try { - return setDeviceConnectionState(state, parcel, codecFormat); + return setDeviceConnectionState(state, parcel, codecFormat, deviceSwitch); } finally { parcel.recycle(); } @@ -1769,7 +1777,10 @@ public class AudioSystem * @hide */ @UnsupportedAppUsage - public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat); + public static native int setDeviceConnectionState(int state, Parcel parcel, int codecFormat, + boolean deviceSwitch); + + /** @hide */ @UnsupportedAppUsage public static native int getDeviceConnectionState(int device, String device_address); diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java index fb1b5b57cce6..15c832392a22 100644 --- a/media/java/android/media/MediaCodec.java +++ b/media/java/android/media/MediaCodec.java @@ -4060,6 +4060,7 @@ final public class MediaCodec { * Finish building a queue request and queue the buffers with tunings. */ public void queue() { + Trace.traceBegin(Trace.TRACE_TAG_VIDEO, "MediaCodec::queueRequest-queue#java"); if (!isAccessible()) { throw new IllegalStateException("The request is stale"); } @@ -4088,6 +4089,7 @@ final public class MediaCodec { mTuningKeys, mTuningValues); } clear(); + Trace.traceEnd(Trace.TRACE_TAG_VIDEO); } @NonNull QueueRequest clear() { diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index 3104f9d42891..e94fb7d9e52b 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -55,6 +55,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -96,12 +97,10 @@ public abstract class MediaRoute2ProviderService extends Service { * system media, as described by {@link MediaRoute2Info#getSupportedRoutingTypes()}. * * @see #onCreateSystemRoutingSession - * @hide */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY) - public static final String SERVICE_INTERFACE_SYSTEM_MEDIA = + public static final String CATEGORY_SYSTEM_MEDIA = "android.media.MediaRoute2ProviderService.SYSTEM_MEDIA"; /** @@ -165,9 +164,7 @@ public abstract class MediaRoute2ProviderService extends Service { * The request has failed because the requested operation is not implemented by the provider. * * @see #notifyRequestFailed - * @hide */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) public static final int REASON_UNIMPLEMENTED = 5; @@ -175,9 +172,7 @@ public abstract class MediaRoute2ProviderService extends Service { * The request has failed because the provider has failed to route system media. * * @see #notifyRequestFailed - * @hide */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) public static final int REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA = 6; @@ -217,7 +212,7 @@ public abstract class MediaRoute2ProviderService extends Service { * package (for example, if they affect the entire system). */ @GuardedBy("mRequestIdsLock") - private final LongSparseArray<Integer> mSystemMediaSessionCreationRequests = + private final LongSparseArray<Integer> mSystemRoutingSessionCreationRequests = new LongSparseArray<>(); @GuardedBy("mSessionLock") @@ -350,7 +345,7 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Notifies the system of the successful creation of a system media routing session. * - * <p>This method can only be called as the result of a prior call to {@link + * <p>This method must only be called as the result of a prior call to {@link * #onCreateSystemRoutingSession}. * * @param requestId the ID of the {@link #onCreateSystemRoutingSession} request which this call @@ -365,13 +360,13 @@ public abstract class MediaRoute2ProviderService extends Service { * where you can clean up this session. {@link AudioRecord#startRecording()} must be called * immediately on {@link MediaStreams#getAudioRecord()} after calling this method, in order * to start streaming audio to the receiver. - * @hide + * @throws IllegalStateException If the provided {@code requestId} doesn't correspond to a + * previous call to {@link #onCreateSystemRoutingSession}. */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING) @Nullable - public final MediaStreams notifySystemMediaSessionCreated( + public final MediaStreams notifySystemRoutingSessionCreated( long requestId, @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaStreamsFormats formats) { @@ -380,7 +375,7 @@ public abstract class MediaRoute2ProviderService extends Service { if (DEBUG) { Log.d( TAG, - "notifySystemMediaSessionCreated: Creating a session. requestId=" + "notifySystemRoutingSessionCreated: Creating a session. requestId=" + requestId + ", sessionInfo=" + sessionInfo); @@ -388,8 +383,8 @@ public abstract class MediaRoute2ProviderService extends Service { Integer uid; synchronized (mRequestIdsLock) { - uid = mSystemMediaSessionCreationRequests.get(requestId); - mSystemMediaSessionCreationRequests.remove(requestId); + uid = mSystemRoutingSessionCreationRequests.get(requestId); + mSystemRoutingSessionCreationRequests.remove(requestId); } if (uid == null) { @@ -656,37 +651,34 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Called when the service receives a request to create a system routing session. * - * <p>This method will only be called for routes that support routing of the system media, as - * described by {@link MediaRoute2Info#getSupportedRoutingTypes()}. + * <p>This method must be overridden by subclasses that support routes that support routing + * {@link MediaRoute2Info#getSupportedRoutingTypes() system media}. The provided {@code routeId} + * will always correspond to a route that supports routing of the system media, as per {@link + * MediaRoute2Info#getSupportedRoutingTypes()}. * - * <p>Implementors of this method must call {@link #notifySystemMediaSessionCreated} with the + * <p>Implementors of this method must call {@link #notifySystemRoutingSessionCreated} with the * given {@code requestId} to indicate a successful session creation. If the session creation * fails (for example, if the connection to the receiver device fails), the implementor must * call {@link #notifyRequestFailed}, passing the {@code requestId}. * * <p>Unlike {@link #onCreateSession}, system sessions route the system media (for example, * audio and/or video) which is to be retrieved by calling {@link - * #notifySystemMediaSessionCreated}. + * #notifySystemRoutingSessionCreated}. * * <p>Changes to the session can be notified by calling {@link #notifySessionUpdated}. * * @param requestId the ID of this request - * @param packageName the package name of the application whose media to route. * @param routeId the ID of the route initially being {@link * RoutingSessionInfo#getSelectedRoutes() selected}. - * @param sessionHints an optional bundle of arguments sent by {@link MediaRouter2}, or null if - * none. + * @param parameters {@link SystemRoutingSessionParams} for the session creation. * @see RoutingSessionInfo.Builder - * @see #notifySystemMediaSessionCreated - * @hide + * @see #notifySystemRoutingSessionCreated */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) public void onCreateSystemRoutingSession( long requestId, - @NonNull String packageName, @NonNull String routeId, - @Nullable Bundle sessionHints) { + @NonNull SystemRoutingSessionParams parameters) { mHandler.post(() -> notifyRequestFailed(requestId, REASON_UNIMPLEMENTED)); } @@ -974,24 +966,29 @@ public abstract class MediaRoute2ProviderService extends Service { int uid, String packageName, String routeId, - @Nullable Bundle sessionHints) { - if (!checkCallerIsSystem()) { + @Nullable Bundle extras) { + if (!Flags.enableMirroringInMediaRouter2() || !checkCallerIsSystem()) { return; } if (!checkRouteIdIsValid(routeId, "requestCreateSession")) { return; } synchronized (mRequestIdsLock) { - mSystemMediaSessionCreationRequests.put(requestId, uid); + mSystemRoutingSessionCreationRequests.put(requestId, uid); } + var sessionParamsBuilder = + new SystemRoutingSessionParams.Builder().setPackageName(packageName); + if (extras != null) { + sessionParamsBuilder.setExtras(extras); + } + var sessionParams = sessionParamsBuilder.build(); mHandler.sendMessage( obtainMessage( MediaRoute2ProviderService::onCreateSystemRoutingSession, MediaRoute2ProviderService.this, requestId, - packageName, routeId, - sessionHints)); + sessionParams)); } @Override @@ -1072,14 +1069,12 @@ public abstract class MediaRoute2ProviderService extends Service { } /** - * Holds the streams to be routed as part of a system media routing session. - * - * <p>The encoded data format matches the {@link MediaStreamsFormats} passed to {@link - * #notifySystemMediaSessionCreated}. + * Holds the streams to be routed as part of a {@link #onCreateSystemRoutingSession system media + * routing session}. * - * @hide + * <p>The encoded data format will match the {@link MediaStreamsFormats} passed to {@link + * #notifySystemRoutingSessionCreated}. */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) public static final class MediaStreams { @@ -1088,8 +1083,6 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Holds the last {@link RoutingSessionInfo} associated with these streams. - * - * @hide */ @NonNull // Access guarded by mSessionsLock, but it's not convenient to enforce through @GuardedBy. @@ -1147,15 +1140,91 @@ public abstract class MediaRoute2ProviderService extends Service { } } + /** + * Holds parameters associated with a {@link #onCreateSystemRoutingSession session creation + * request}. + */ + @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) + public static final class SystemRoutingSessionParams { + + private final String mPackageName; + private final Bundle mExtras; + + private SystemRoutingSessionParams(Builder builder) { + this.mPackageName = builder.mPackageName; + this.mExtras = builder.mExtras; + } + + /** + * Returns the name of the package associated with the session, or an empty string if not + * applicable. + * + * <p>The package name is not applicable if the session is not associated with a specific + * package, for example is the session affects the entire system. + */ + @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** Returns a bundle provided by the client that triggered the session creation request. */ + @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** A builder for {@link SystemRoutingSessionParams}. */ + public static final class Builder { + private String mPackageName; + private Bundle mExtras; + + /** Constructor. */ + public Builder() { + mPackageName = ""; + mExtras = Bundle.EMPTY; + } + + /** + * Sets the {@link #getExtras() extras}. + * + * <p>The default value is an empty {@link Bundle}. + * + * <p>Note that this bundle is not copied, so avoiding mutating the given {@link Bundle} + * after passing it to this method. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = Objects.requireNonNull(extras); + return this; + } + + /** + * Sets the {@link #getPackageName()}. + * + * <p>The default value is an empty string. + */ + @NonNull + public Builder setPackageName(@NonNull String packageName) { + mPackageName = Objects.requireNonNull(packageName); + return this; + } + + /** Returns a new {@link SystemRoutingSessionParams} instance. */ + @NonNull + public SystemRoutingSessionParams build() { + return new SystemRoutingSessionParams(this); + } + } + } /** * Holds the formats to encode media data to be read from {@link MediaStreams}. * * @see MediaStreams - * @see #notifySystemMediaSessionCreated - * @hide + * @see #notifySystemRoutingSessionCreated */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) public static final class MediaStreamsFormats { @@ -1169,29 +1238,25 @@ public abstract class MediaRoute2ProviderService extends Service { /** * Returns the audio format to use for creating the {@link MediaStreams#getAudioRecord} to - * return from {@link #notifySystemMediaSessionCreated}. - * - * @hide + * return from {@link #notifySystemRoutingSessionCreated}. May be null if the session + * doesn't support system audio. */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) + @Nullable public AudioFormat getAudioFormat() { return mAudioFormat; } /** * Builder for {@link MediaStreamsFormats} - * - * @hide */ - // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) public static final class Builder { private AudioFormat mAudioFormat; /** * Sets the audio format to use for creating the {@link MediaStreams#getAudioRecord} to - * return from {@link #notifySystemMediaSessionCreated}. + * return from {@link #notifySystemRoutingSessionCreated}. * * @param audioFormat the audio format * @return this builder diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 0f24654879cd..021348153bb8 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -60,6 +60,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * RingtoneManager provides access to ringtones, notification, and other types @@ -810,9 +811,7 @@ public class RingtoneManager { // Don't set the stream type Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */, volumeShaperConfig, false); - if (Flags.enableRingtoneHapticsCustomization() - && Utils.isRingtoneVibrationSettingsSupported(context) - && Utils.hasVibration(ringtoneUri) && hasHapticChannels(ringtoneUri)) { + if (muteHapticChannelForVibration(context, ringtoneUri)) { audioAttributes = new AudioAttributes.Builder( audioAttributes).setHapticChannelsMuted(true).build(); } @@ -1305,4 +1304,19 @@ public class RingtoneManager { default: throw new IllegalArgumentException(); } } + + private static boolean muteHapticChannelForVibration(Context context, Uri ringtoneUri) { + final Uri vibrationUri = Utils.getVibrationUri(ringtoneUri); + // No vibration is specified + if (vibrationUri == null) { + return false; + } + // The user specified the synchronized pattern + if (Objects.equals(vibrationUri.toString(), Utils.SYNCHRONIZED_VIBRATION)) { + return false; + } + return Flags.enableRingtoneHapticsCustomization() + && Utils.isRingtoneVibrationSettingsSupported(context) + && hasHapticChannels(ringtoneUri); + } } diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java index 11bd221ec696..d6e27b0ffa75 100644 --- a/media/java/android/media/Utils.java +++ b/media/java/android/media/Utils.java @@ -66,6 +66,8 @@ public class Utils { public static final String VIBRATION_URI_PARAM = "vibration_uri"; + public static final String SYNCHRONIZED_VIBRATION = "synchronized"; + /** * Sorts distinct (non-intersecting) range array in ascending order. * @throws java.lang.IllegalArgumentException if ranges are not distinct @@ -757,8 +759,8 @@ public class Utils { return null; } String filePath = vibrationUri.getPath(); - if (filePath == null) { - Log.w(TAG, "The file path is null."); + if (filePath == null || filePath.equals(Utils.SYNCHRONIZED_VIBRATION)) { + Log.w(TAG, "Ignore the vibration parsing for file:" + filePath); return null; } File vibrationFile = new File(filePath); diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp index b56b944955d7..af8e856a5ad6 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp @@ -35,5 +35,6 @@ android_library { "com.android.extservices", "com.android.permission", "com.android.healthfitness", + "com.android.mediaprovider", ], } diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt index dbac17d4e8b8..44c93c77e33b 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt @@ -74,8 +74,14 @@ interface BooleanValuePreferenceBinding : PreferenceBinding { override fun bind(preference: Preference, metadata: PreferenceMetadata) { super.bind(preference, metadata) (preference as TwoStatePreference).apply { + // MUST suppress persistent when initializing the checked state: + // 1. default value is written to datastore if not set (b/396260949) + // 2. avoid redundant read to the datastore + val suppressPersistent = isPersistent + if (suppressPersistent) isPersistent = false // "false" is kind of placeholder, metadata datastore should provide the default value isChecked = preferenceDataStore!!.getBoolean(key, false) + if (suppressPersistent) isPersistent = true } } } diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt index bfaeb42d5a31..8d12f01e24ed 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt @@ -17,7 +17,9 @@ package com.android.settingslib.widget import android.os.Bundle +import android.view.LayoutInflater; import android.view.View +import android.view.ViewGroup; import androidx.annotation.CallSuper import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen @@ -27,6 +29,15 @@ import androidx.recyclerview.widget.RecyclerView abstract class SettingsBasePreferenceFragment : PreferenceFragmentCompat() { @CallSuper + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return super.onCreateView(inflater, container, savedInstanceState) + } + + @CallSuper override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { diff --git a/packages/SettingsLib/SettingsTransition/Android.bp b/packages/SettingsLib/SettingsTransition/Android.bp index e04af6c1ab11..6b9cbfa8ece7 100644 --- a/packages/SettingsLib/SettingsTransition/Android.bp +++ b/packages/SettingsLib/SettingsTransition/Android.bp @@ -30,5 +30,6 @@ android_library { "com.android.extservices", "com.android.permission", "com.android.healthfitness", + "com.android.mediaprovider", ], } diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp index 76e36dc5ff7d..bf26264a4f0e 100644 --- a/packages/SettingsLib/TopIntroPreference/Android.bp +++ b/packages/SettingsLib/TopIntroPreference/Android.bp @@ -32,5 +32,6 @@ android_library { "com.android.cellbroadcast", "com.android.devicelock", "com.android.healthfitness", + "com.android.mediaprovider", ], } diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml index d9a417f1ea99..54c878810d10 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,24 +14,24 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="14dp" - android:height="14dp" - android:viewportWidth="14.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> <path - android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z" - android:fillAlpha="0.24" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z" - android:fillAlpha="0.24" + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z" - android:fillAlpha="0.24" + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z" - android:fillAlpha="0.24" + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml index facc285a45ca..6015be8c894f 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_0_4_bar_error.xml @@ -1,28 +1,51 @@ +<!-- + Copyright (C) 2025 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="15dp" - android:height="14dp" - android:viewportWidth="15.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path + android:pathData=" + M0,0 + V13.5,0 + H13.5,20 + V0,20 + H0,0 + M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> + <path + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + <path + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + <path + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + <path + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + </group> <path - android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z" - android:fillColor="#000"/> - <path - android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z" + android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml index 2c05a938c2cf..7c85b9e44383 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar.xml @@ -14,28 +14,28 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="16dp" - android:height="14dp" - android:viewportWidth="16.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportWidth="20.5" + android:viewportHeight="12.0"> <path - android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z" - android:fillAlpha="0.24" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z" - android:fillAlpha="0.24" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z" - android:fillAlpha="0.24" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z" - android:fillAlpha="0.24" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z" - android:fillAlpha="0.24" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml index 328e45ec7e19..f75ec57f2a5e 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_0_5_bar_error.xml @@ -1,32 +1,54 @@ +<!-- + Copyright (C) 2025 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="18dp" - android:height="14dp" - android:viewportWidth="18.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportHeight="12.0" + android:viewportWidth="20.5"> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <group> + <clip-path android:pathData=" + M0,0 + H20.5 + V12.0 + H0 + Z + M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" /> + </group> <path - android:pathData="M14,0.5C14,0.224 14.224,0 14.5,0H15.5C15.776,0 16,0.224 16,0.5V3H14V0.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M0.5,11C0.224,11 0,11.224 0,11.5V13.5C0,13.776 0.224,14 0.5,14H1.5C1.776,14 2,13.776 2,13.5V11.5C2,11.224 1.776,11 1.5,11H0.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M4,8C3.724,8 3.5,8.224 3.5,8.5V13.5C3.5,13.776 3.724,14 4,14H5C5.276,14 5.5,13.776 5.5,13.5V8.5C5.5,8.224 5.276,8 5,8H4Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z" - android:fillColor="#000"/> - <path - android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" /> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml index b9054ba7a4e3..df89aef3b63d 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar.xml @@ -14,23 +14,23 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="14dp" - android:height="14dp" - android:viewportWidth="14.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> <path - android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z" - android:fillAlpha="0.24" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z" - android:fillAlpha="0.24" + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z" + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z" - android:fillAlpha="0.24" + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml index 03a93491491c..fb73b6b253e1 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_1_4_bar_error.xml @@ -1,27 +1,35 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="15dp" - android:height="14dp" - android:viewportWidth="15.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path + android:pathData=" + M0,0 + V13.5,0 + H13.5,20 + V0,20 + H0,0 + M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> + <path + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" + android:fillColor="#000"/> + <path + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + <path + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + <path + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + </group> <path - android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z" - android:fillColor="#000"/> - <path - android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z" + android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml index 774e91794df3..5b7d8daa74f2 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar.xml @@ -14,27 +14,27 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="16dp" - android:height="14dp" - android:viewportWidth="16.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportWidth="20.5" + android:viewportHeight="12.0"> <path - android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z" - android:fillAlpha="0.24" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z" - android:fillAlpha="0.24" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z" - android:fillAlpha="0.24" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z" - android:fillAlpha="0.24" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml index 343ec1b2e50f..27e233d244c7 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_1_5_bar_error.xml @@ -1,31 +1,53 @@ +<!-- + Copyright (C) 2025 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="18dp" - android:height="14dp" - android:viewportWidth="18.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportHeight="12.0" + android:viewportWidth="20.5"> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <group> + <clip-path android:pathData=" + M0,0 + H20.5 + V12.0 + H0 + Z + M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" /> + <path + android:fillColor="#000" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" /> + </group> <path - android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z" - android:fillColor="#000"/> - <path - android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" /> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml index b699203dd652..e7ebf6f6883a 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,22 +14,22 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="14dp" - android:height="14dp" - android:viewportWidth="14.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> <path - android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z" - android:fillAlpha="0.24" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z" - android:fillAlpha="0.24" + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" android:fillColor="#000"/> <path - android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z" + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z" + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml index ba8649b23b38..49ae9e4ef17f 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_2_4_bar_error.xml @@ -1,26 +1,49 @@ +<!-- + Copyright (C) 2025 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="15dp" - android:height="14dp" - android:viewportWidth="15.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path + android:pathData=" + M0,0 + V13.5,0 + H13.5,20 + V0,20 + H0,0 + M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> + <path + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" + android:fillColor="#000"/> + <path + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" + android:fillColor="#000"/> + <path + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + <path + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + </group> <path - android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z" - android:fillColor="#000"/> - <path - android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z" - android:fillColor="#000"/> - <path - android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z" + android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml index 43fa734c0c8d..19387bc6c75c 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,26 +14,26 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="16dp" - android:height="14dp" - android:viewportWidth="16.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportWidth="20.5" + android:viewportHeight="12.0"> <path - android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z" - android:fillAlpha="0.24" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z" - android:fillAlpha="0.24" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" android:fillColor="#000"/> <path - android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z" - android:fillAlpha="0.24" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml index 6309e1772d4a..322ede67de5b 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_2_5_bar_error.xml @@ -1,30 +1,52 @@ +<!-- + Copyright (C) 2025 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="18dp" - android:height="14dp" - android:viewportWidth="18.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportHeight="12.0" + android:viewportWidth="20.5"> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <group> + <clip-path android:pathData=" + M0,0 + H20.5 + V12.0 + H0 + Z + M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" /> + <path + android:fillColor="#000" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" /> + <path + android:fillColor="#000" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" /> + </group> <path - android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z" - android:fillColor="#000"/> - <path - android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z" - android:fillColor="#000"/> - <path - android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" /> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml index 6a218b310b3a..b84b6583e8aa 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,21 +14,21 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="14dp" - android:height="14dp" - android:viewportWidth="14.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> <path - android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z" - android:fillAlpha="0.24" + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" android:fillColor="#000"/> <path - android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z" + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" android:fillColor="#000"/> <path - android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z" + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml index 27433c79e8bb..7c4c1c6b1126 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_3_4_bar_error.xml @@ -1,25 +1,48 @@ +<!-- + Copyright (C) 2025 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="15dp" - android:height="14dp" - android:viewportWidth="15.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path + android:pathData=" + M0,0 + V13.5,0 + H13.5,20 + V0,20 + H0,0 + M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> + <path + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" + android:fillColor="#000"/> + <path + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" + android:fillColor="#000"/> + <path + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillColor="#000"/> + <path + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillAlpha="0.45" + android:fillColor="#000"/> + </group> <path - android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z" - android:fillColor="#000"/> - <path - android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z" - android:fillColor="#000"/> - <path - android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z" - android:fillColor="#000"/> - <path - android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z" + android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml index 158ae016ffb5..973032f3c237 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,25 +14,25 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="16dp" - android:height="14dp" - android:viewportWidth="16.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportWidth="20.5" + android:viewportHeight="12.0"> <path - android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z" - android:fillAlpha="0.24" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" android:fillColor="#000"/> <path - android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" android:fillColor="#000"/> <path - android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z" - android:fillAlpha="0.24" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" + android:fillAlpha="0.45" android:fillColor="#000"/> <path - android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml index e0517cfdfeee..25c9520ad011 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_3_5_bar_error.xml @@ -1,29 +1,51 @@ +<!-- + Copyright (C) 2025 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="18dp" - android:height="14dp" - android:viewportWidth="18.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportHeight="12.0" + android:viewportWidth="20.5"> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <group> + <clip-path android:pathData=" + M0,0 + H20.5 + V12.0 + H0 + Z + M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" /> + <path + android:fillColor="#000" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" /> + <path + android:fillColor="#000" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" /> + <path + android:fillColor="#000" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" /> + </group> <path - android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z" - android:fillColor="#000"/> - <path - android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z" - android:fillColor="#000"/> - <path - android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z" - android:fillColor="#000"/> - <path - android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" /> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml index 1ebd3965f36f..fc807fa90b44 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,20 +14,20 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="14dp" - android:height="14dp" - android:viewportWidth="14.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> <path - android:pathData="M8.25,3L9.25,3A0.5,0.5 0,0 1,9.75 3.5L9.75,13.5A0.5,0.5 0,0 1,9.25 14L8.25,14A0.5,0.5 0,0 1,7.75 13.5L7.75,3.5A0.5,0.5 0,0 1,8.25 3z" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11.75,0L12.75,0A0.5,0.5 0,0 1,13.25 0.5L13.25,13.5A0.5,0.5 0,0 1,12.75 14L11.75,14A0.5,0.5 0,0 1,11.25 13.5L11.25,0.5A0.5,0.5 0,0 1,11.75 0z" + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" android:fillColor="#000"/> <path - android:pathData="M1.25,10L2.25,10A0.5,0.5 0,0 1,2.75 10.5L2.75,13.5A0.5,0.5 0,0 1,2.25 14L1.25,14A0.5,0.5 0,0 1,0.75 13.5L0.75,10.5A0.5,0.5 0,0 1,1.25 10z" + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" android:fillColor="#000"/> <path - android:pathData="M4.75,6.5L5.75,6.5A0.5,0.5 0,0 1,6.25 7L6.25,13.5A0.5,0.5 0,0 1,5.75 14L4.75,14A0.5,0.5 0,0 1,4.25 13.5L4.25,7A0.5,0.5 0,0 1,4.75 6.5z" + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml index 4473c29d0866..d23680d17b9c 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_4_4_bar_error.xml @@ -1,24 +1,47 @@ +<!-- + Copyright (C) 2025 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="15dp" - android:height="14dp" - android:viewportWidth="15.0" - android:viewportHeight="14.0"> + android:width="17dp" + android:height="12dp" + android:viewportWidth="16.0" + android:viewportHeight="12.0"> + <group> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <clip-path + android:pathData=" + M0,0 + V13.5,0 + H13.5,20 + V0,20 + H0,0 + M14.999,13.5C17.761,13.5 19.999,11.261 19.999,8.5C19.999,5.739 17.761,3.5 14.999,3.5C12.238,3.5 9.999,5.739 9.999,8.5C9.999,11.261 12.238,13.5 14.999,13.5Z" /> + <path + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" + android:fillColor="#000"/> + <path + android:pathData="M5.749,4.5C6.439,4.5 6.999,5.06 6.999,5.75L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,5.75C4.499,5.06 5.059,4.5 5.749,4.5Z" + android:fillColor="#000"/> + <path + android:pathData="M10.249,2C10.939,2 11.499,2.56 11.499,3.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,3.25C8.999,2.56 9.559,2 10.249,2Z" + android:fillColor="#000"/> + <path + android:pathData="M14.749,0C15.439,0 15.999,0.56 15.999,1.25L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,1.25C13.499,0.56 14.059,0 14.749,0Z" + android:fillColor="#000"/> + </group> <path - android:pathData="M7,3.5C7,3.224 7.224,3 7.5,3H8.5C8.776,3 9,3.224 9,3.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V3.5Z" - android:fillColor="#000"/> - <path - android:pathData="M0,10.5C0,10.224 0.224,10 0.5,10H1.5C1.776,10 2,10.224 2,10.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V10.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,7C3.5,6.724 3.724,6.5 4,6.5H5C5.276,6.5 5.5,6.724 5.5,7V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V7Z" - android:fillColor="#000"/> - <path - android:pathData="M11,0C10.724,0 10.5,0.224 10.5,0.5V3H12.5V0.5C12.5,0.224 12.276,0 12,0H11Z" - android:fillColor="#000"/> - <path - android:pathData="M12.25,13C12.25,12.448 12.698,12 13.25,12C13.802,12 14.25,12.448 14.25,13C14.25,13.552 13.802,14 13.25,14C12.698,14 12.25,13.552 12.25,13Z" - android:fillColor="#000"/> - <path - android:pathData="M12.25,5C12.25,4.724 12.474,4.5 12.75,4.5H13.75C14.026,4.5 14.25,4.724 14.25,5V10C14.25,10.276 14.026,10.5 13.75,10.5H12.75C12.474,10.5 12.25,10.276 12.25,10V5Z" + android:pathData="M14.999,5C14.589,5 14.249,5.34 14.249,5.75L14.249,8.75C14.249,9.16 14.589,9.5 14.999,9.5C15.409,9.5 15.749,9.16 15.749,8.75L15.749,5.75C15.749,5.34 15.409,5 14.999,5ZM14.999,12C15.409,12 15.749,11.66 15.749,11.25C15.749,10.84 15.409,10.5 14.999,10.5C14.589,10.5 14.249,10.84 14.249,11.25C14.249,11.66 14.589,12 14.999,12Z" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml index 1ed6ac86b21a..b1336d70fc39 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,24 +14,24 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="16dp" - android:height="14dp" - android:viewportWidth="16.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportWidth="20.5" + android:viewportHeight="12.0"> <path - android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" android:fillColor="#000"/> <path - android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" android:fillColor="#000"/> <path - android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z" - android:fillAlpha="0.24" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" android:fillColor="#000"/> <path - android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" + android:fillAlpha="0.45" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml index 703e3acd5f75..bf62535b9574 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_4_5_bar_error.xml @@ -1,28 +1,50 @@ +<!-- + Copyright (C) 2025 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="18dp" - android:height="14dp" - android:viewportWidth="18.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportHeight="12.0" + android:viewportWidth="20.5"> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <group> + <clip-path android:pathData=" + M0,0 + H20.5 + V12.0 + H0 + Z + M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" /> + <path + android:fillColor="#000" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" /> + <path + android:fillColor="#000" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" /> + <path + android:fillColor="#000" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" /> + <path + android:fillColor="#000" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" /> + <path + android:fillAlpha="0.45" + android:fillColor="#000" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" /> + </group> <path - android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z" - android:fillColor="#000"/> - <path - android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z" - android:fillColor="#000"/> - <path - android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z" - android:fillColor="#000"/> - <path - android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z" - android:fillAlpha="0.3" - android:fillColor="#000"/> - <path - android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z" - android:fillColor="#000"/> - <path - android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" /> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml index 420ffb601e8f..fa9bedc021d8 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar.xml @@ -1,5 +1,5 @@ <!-- - Copyright (C) 2023 The Android Open Source Project + Copyright (C) 2025 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,23 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="16dp" - android:height="14dp" - android:viewportWidth="16.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportWidth="20.5" + android:viewportHeight="12.0"> <path - android:pathData="M7.5,5L8.5,5A0.5,0.5 0,0 1,9 5.5L9,13.5A0.5,0.5 0,0 1,8.5 14L7.5,14A0.5,0.5 0,0 1,7 13.5L7,5.5A0.5,0.5 0,0 1,7.5 5z" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" android:fillColor="#000"/> <path - android:pathData="M11,2L12,2A0.5,0.5 0,0 1,12.5 2.5L12.5,13.5A0.5,0.5 0,0 1,12 14L11,14A0.5,0.5 0,0 1,10.5 13.5L10.5,2.5A0.5,0.5 0,0 1,11 2z" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" android:fillColor="#000"/> <path - android:pathData="M0.5,11L1.5,11A0.5,0.5 0,0 1,2 11.5L2,13.5A0.5,0.5 0,0 1,1.5 14L0.5,14A0.5,0.5 0,0 1,0 13.5L0,11.5A0.5,0.5 0,0 1,0.5 11z" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" android:fillColor="#000"/> <path - android:pathData="M14.5,0L15.5,0A0.5,0.5 0,0 1,16 0.5L16,13.5A0.5,0.5 0,0 1,15.5 14L14.5,14A0.5,0.5 0,0 1,14 13.5L14,0.5A0.5,0.5 0,0 1,14.5 0z" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" android:fillColor="#000"/> <path - android:pathData="M4,8L5,8A0.5,0.5 0,0 1,5.5 8.5L5.5,13.5A0.5,0.5 0,0 1,5 14L4,14A0.5,0.5 0,0 1,3.5 13.5L3.5,8.5A0.5,0.5 0,0 1,4 8z" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" android:fillColor="#000"/> </vector> diff --git a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml index e63ca77e9db1..1728bc78b22b 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_5_5_bar_error.xml @@ -1,27 +1,49 @@ +<!-- + Copyright (C) 2025 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="18dp" - android:height="14dp" - android:viewportWidth="18.0" - android:viewportHeight="14.0"> + android:width="21dp" + android:height="12dp" + android:viewportHeight="12.0" + android:viewportWidth="20.5"> + <!-- clip-out the circle which will contain the exclamation point (below this group) --> + <group> + <clip-path android:pathData=" + M0,0 + H20.5 + V12.0 + H0 + Z + M19.499,13.5C22.261,13.5 24.499,11.261 24.499,8.5C24.499,5.739 22.261,3.5 19.499,3.5C16.738,3.5 14.499,5.739 14.499,8.5C14.499,11.261 16.738,13.5 19.499,13.5Z" /> + <path + android:fillColor="#000" + android:pathData="M1.249,7C1.939,7 2.499,7.56 2.499,8.25L2.499,10.75C2.499,11.44 1.939,12 1.249,12C0.559,12 -0.001,11.44 -0.001,10.75L-0.001,8.25C-0.001,7.56 0.559,7 1.249,7Z" /> + <path + android:fillColor="#000" + android:pathData="M5.749,5C6.439,5 6.999,5.56 6.999,6.25L6.999,10.75C6.999,11.44 6.439,12 5.749,12C5.059,12 4.499,11.44 4.499,10.75L4.499,6.25C4.499,5.56 5.059,5 5.749,5Z" /> + <path + android:fillColor="#000" + android:pathData="M10.249,3C10.939,3 11.499,3.56 11.499,4.25L11.499,10.75C11.499,11.44 10.939,12 10.249,12C9.559,12 8.999,11.44 8.999,10.75L8.999,4.25C8.999,3.56 9.559,3 10.249,3Z" /> + <path + android:fillColor="#000" + android:pathData="M14.749,1.5C15.439,1.5 15.999,2.06 15.999,2.75L15.999,10.75C15.999,11.44 15.439,12 14.749,12C14.059,12 13.499,11.44 13.499,10.75L13.499,2.75C13.499,2.06 14.059,1.5 14.749,1.5Z" /> + <path + android:fillColor="#000" + android:pathData="M19.249,0C19.939,0 20.499,0.56 20.499,1.25L20.499,10.75C20.499,11.44 19.939,12 19.249,12C18.559,12 17.999,11.44 17.999,10.75L17.999,1.25C17.999,0.56 18.559,0 19.249,0Z" /> + </group> <path - android:pathData="M7,5.5C7,5.224 7.224,5 7.5,5H8.5C8.776,5 9,5.224 9,5.5V13.5C9,13.776 8.776,14 8.5,14H7.5C7.224,14 7,13.776 7,13.5V5.5Z" - android:fillColor="#000"/> - <path - android:pathData="M10.5,2.5C10.5,2.224 10.724,2 11,2H12C12.276,2 12.5,2.224 12.5,2.5V13.5C12.5,13.776 12.276,14 12,14H11C10.724,14 10.5,13.776 10.5,13.5V2.5Z" - android:fillColor="#000"/> - <path - android:pathData="M0,11.5C0,11.224 0.224,11 0.5,11H1.5C1.776,11 2,11.224 2,11.5V13.5C2,13.776 1.776,14 1.5,14H0.5C0.224,14 0,13.776 0,13.5V11.5Z" - android:fillColor="#000"/> - <path - android:pathData="M3.5,8.5C3.5,8.224 3.724,8 4,8H5C5.276,8 5.5,8.224 5.5,8.5V13.5C5.5,13.776 5.276,14 5,14H4C3.724,14 3.5,13.776 3.5,13.5V8.5Z" - android:fillColor="#000"/> - <path - android:pathData="M14.5,0C14.224,0 14,0.224 14,0.5V3H16V0.5C16,0.224 15.776,0 15.5,0H14.5Z" - android:fillColor="#000"/> - <path - android:pathData="M16,13C16,12.448 16.448,12 17,12C17.552,12 18,12.448 18,13C18,13.552 17.552,14 17,14C16.448,14 16,13.552 16,13Z" - android:fillColor="#000"/> - <path - android:pathData="M16,5C16,4.724 16.224,4.5 16.5,4.5H17.5C17.776,4.5 18,4.724 18,5V10C18,10.276 17.776,10.5 17.5,10.5H16.5C16.224,10.5 16,10.276 16,10V5Z" - android:fillColor="#000"/> + android:fillColor="#000" + android:pathData="M19.499,5C19.089,5 18.749,5.34 18.749,5.75L18.749,8.75C18.749,9.16 19.089,9.5 19.499,9.5C19.909,9.5 20.249,9.16 20.249,8.75L20.249,5.75C20.249,5.34 19.909,5 19.499,5ZM19.499,12C19.909,12 20.249,11.66 20.249,11.25C20.249,10.84 19.909,10.5 19.499,10.5C19.089,10.5 18.749,10.84 18.749,11.25C18.749,11.66 19.089,12 19.499,12Z" /> </vector> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 4b0400fb3441..91ec83690722 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -889,6 +889,9 @@ <!-- Preference category for monitoring debugging development settings. [CHAR LIMIT=25] --> <string name="debug_monitoring_category">Monitoring</string> + <!-- Preference category to alter window management settings, [CHAR LIMIT=50] --> + <string name="window_management_category">Window Management</string> + <!-- UI debug setting: always enable strict mode? [CHAR LIMIT=25] --> <string name="strict_mode">Strict mode enabled</string> <!-- UI debug setting: show strict mode summary [CHAR LIMIT=50] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 7374f80fd9db..bb96041739eb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -16,8 +16,7 @@ package com.android.settingslib.bluetooth; -import static com.android.settingslib.flags.Flags.enableSetPreferredTransportForLeAudioDevice; -import static com.android.settingslib.flags.Flags.ignoreA2dpDisconnectionForAndroidAuto; +import static com.android.settingslib.media.flags.Flags.enableTvMediaOutputDialog; import android.annotation.CallbackExecutor; import android.annotation.StringRes; @@ -53,7 +52,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.settingslib.R; import com.android.settingslib.Utils; -import com.android.settingslib.media.flags.Flags; +import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.AdaptiveOutlineDrawable; @@ -264,7 +263,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mHandler.removeMessages(profile.getProfileId()); if (profile.getConnectionPolicy(mDevice) > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { - if (ignoreA2dpDisconnectionForAndroidAuto() + if (Flags.ignoreA2dpDisconnectionForAndroidAuto() && profile instanceof A2dpProfile && isAndroidAuto()) { Log.w(TAG, "onProfileStateChanged(): Skip setting A2DP " @@ -306,7 +305,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mLocalNapRoleConnected = true; } } - if (enableSetPreferredTransportForLeAudioDevice() + if (Flags.enableSetPreferredTransportForLeAudioDevice() && profile instanceof HidProfile) { updatePreferredTransport(); } @@ -322,7 +321,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mLocalNapRoleConnected = false; } - if (enableSetPreferredTransportForLeAudioDevice() + if (Flags.enableSetPreferredTransportForLeAudioDevice() && profile instanceof LeAudioProfile) { updatePreferredTransport(); } @@ -1345,6 +1344,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> if (mBluetoothManager == null) { mBluetoothManager = LocalBluetoothManager.getInstance(mContext, null); } + boolean isTempBond = Flags.enableTemporaryBondDevicesUi() + && BluetoothUtils.isTemporaryBondDevice(getDevice()); if (BluetoothUtils.hasConnectedBroadcastSource(this, mBluetoothManager)) { // Gets summary for the buds which are in the audio sharing. int groupId = BluetoothUtils.getGroupId(this); @@ -1363,14 +1364,23 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> shortSummary); } else { // The buds are not primary buds - return getSummaryWithBatteryInfo( - R.string.bluetooth_active_media_only_battery_level_untethered, - R.string.bluetooth_active_media_only_battery_level, - R.string.bluetooth_active_media_only_no_battery_level, - leftBattery, - rightBattery, - batteryLevelPercentageString, - shortSummary); + return isTempBond + ? getSummaryWithBatteryInfo( + R.string.bluetooth_guest_media_only_battery_level_untethered, + R.string.bluetooth_guest_media_only_battery_level, + R.string.bluetooth_guest_media_only_no_battery_level, + leftBattery, + rightBattery, + batteryLevelPercentageString, + shortSummary) + : getSummaryWithBatteryInfo( + R.string.bluetooth_active_media_only_battery_level_untethered, + R.string.bluetooth_active_media_only_battery_level, + R.string.bluetooth_active_media_only_no_battery_level, + leftBattery, + rightBattery, + batteryLevelPercentageString, + shortSummary); } } else { // Gets summary for the buds which are not in the audio sharing. @@ -1381,16 +1391,28 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> && profile.isEnabled(getDevice()))) { // The buds support le audio. if (isConnected()) { - return getSummaryWithBatteryInfo( - R.string.bluetooth_battery_level_untethered_lea_support, - R.string.bluetooth_battery_level_lea_support, - R.string.bluetooth_no_battery_level_lea_support, - leftBattery, - rightBattery, - batteryLevelPercentageString, - shortSummary); + return isTempBond + ? getSummaryWithBatteryInfo( + R.string.bluetooth_guest_battery_level_untethered_lea_support, + R.string.bluetooth_guest_battery_level_lea_support, + R.string.bluetooth_guest_no_battery_level_lea_support, + leftBattery, + rightBattery, + batteryLevelPercentageString, + shortSummary) + : getSummaryWithBatteryInfo( + R.string.bluetooth_battery_level_untethered_lea_support, + R.string.bluetooth_battery_level_lea_support, + R.string.bluetooth_no_battery_level_lea_support, + leftBattery, + rightBattery, + batteryLevelPercentageString, + shortSummary); } else { - return mContext.getString(R.string.bluetooth_saved_device_lea_support); + return isTempBond + ? mContext.getString( + R.string.bluetooth_guest_saved_device_lea_support) + : mContext.getString(R.string.bluetooth_saved_device_lea_support); } } } @@ -1509,11 +1531,19 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> leftBattery = getLeftBatteryLevel(); rightBattery = getRightBatteryLevel(); + boolean isTempBond = Flags.enableTemporaryBondDevicesUi() + && BluetoothUtils.isTemporaryBondDevice(getDevice()); // Set default string with battery level in device connected situation. if (isTwsBatteryAvailable(leftBattery, rightBattery)) { - stringRes = R.string.bluetooth_battery_level_untethered; + stringRes = + isTempBond + ? R.string.bluetooth_guest_battery_level_untethered + : R.string.bluetooth_battery_level_untethered; } else if (batteryLevelPercentageString != null && !shortSummary) { - stringRes = R.string.bluetooth_battery_level; + stringRes = + isTempBond + ? R.string.bluetooth_guest_battery_level + : R.string.bluetooth_battery_level; } // Set active string in following device connected situation, also show battery @@ -1529,11 +1559,20 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> || (mIsActiveDeviceA2dp && !isOnCall) || mIsActiveDeviceLeAudio) { if (isTwsBatteryAvailable(leftBattery, rightBattery) && !shortSummary) { - stringRes = R.string.bluetooth_active_battery_level_untethered; + stringRes = + isTempBond + ? R.string.bluetooth_guest_battery_level_untethered + : R.string.bluetooth_active_battery_level_untethered; } else if (batteryLevelPercentageString != null && !shortSummary) { - stringRes = R.string.bluetooth_active_battery_level; + stringRes = + isTempBond + ? R.string.bluetooth_guest_battery_level + : R.string.bluetooth_active_battery_level; } else { - stringRes = R.string.bluetooth_active_no_battery_level; + stringRes = + isTempBond + ? R.string.bluetooth_guest_no_battery_level + : R.string.bluetooth_active_no_battery_level; } } @@ -1559,7 +1598,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> || stringRes == R.string.bluetooth_active_battery_level_untethered_left || stringRes == R.string.bluetooth_active_battery_level_untethered_right || stringRes == R.string.bluetooth_battery_level_untethered; - if (isTvSummary && summaryIncludesBatteryLevel && Flags.enableTvMediaOutputDialog()) { + if (isTvSummary && summaryIncludesBatteryLevel && enableTvMediaOutputDialog()) { return getTvBatterySummary( getMinBatteryLevelWithMemberDevices(), leftBattery, @@ -1949,6 +1988,17 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } /** + * @return {@code true} if {@code cachedBluetoothDevice} has member which is LeAudio device + */ + public boolean hasConnectedLeAudioMemberDevice() { + LeAudioProfile leAudio = mProfileManager.getLeAudioProfile(); + return leAudio != null && getMemberDevice().stream().anyMatch( + cachedDevice -> cachedDevice != null && cachedDevice.getDevice() != null + && leAudio.getConnectionStatus(cachedDevice.getDevice()) + == BluetoothProfile.STATE_CONNECTED); + } + + /** * @return {@code true} if {@code cachedBluetoothDevice} supports broadcast assistant profile */ public boolean isConnectedLeAudioBroadcastAssistantDevice() { diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java index 13a06017abbc..671dfa230f0d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java @@ -128,12 +128,20 @@ public class SignalDrawable extends DrawableWrapper { @Override public int getIntrinsicWidth() { - return mIntrinsicSize; + if (newStatusBarIcons()) { + return super.getIntrinsicWidth(); + } else { + return mIntrinsicSize; + } } @Override public int getIntrinsicHeight() { - return mIntrinsicSize; + if (newStatusBarIcons()) { + return super.getIntrinsicHeight(); + } else { + return mIntrinsicSize; + } } private void updateAnimation() { diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index f446bb8e32d1..c4e724554c04 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -93,10 +93,9 @@ class ZenModeRepositoryImpl( IntentFilter().apply { addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED) addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) - if (android.app.Flags.modesApi()) - addAction( - NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED - ) + addAction( + NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED + ) }, /* broadcastPermission = */ null, /* scheduler = */ backgroundHandler, @@ -109,16 +108,13 @@ class ZenModeRepositoryImpl( } override val consolidatedNotificationPolicy: StateFlow<NotificationManager.Policy?> by lazy { - if (android.app.Flags.modesApi()) - flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) { - // If available, get the value from extras to avoid a potential binder call. - it?.extras?.getParcelable(EXTRA_NOTIFICATION_POLICY) - ?: notificationManager.consolidatedNotificationPolicy - } - else - flowFromBroadcast(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED) { - notificationManager.consolidatedNotificationPolicy - } + flowFromBroadcast(NotificationManager.ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED) { + // If available, get the value from extras to avoid a potential binder call. + it?.extras?.getParcelable( + EXTRA_NOTIFICATION_POLICY, + NotificationManager.Policy::class.java + ) ?: notificationManager.consolidatedNotificationPolicy + } } override val globalZenMode: StateFlow<Int?> by lazy { diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java index f0e7fb851d5f..52d62b6226b8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/EnableDndDialogFactory.java @@ -19,7 +19,6 @@ package com.android.settingslib.notification.modes; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.AlertDialog; -import android.app.Flags; import android.app.NotificationManager; import android.content.Context; import android.content.DialogInterface; @@ -42,8 +41,6 @@ import android.widget.RadioGroup; import android.widget.ScrollView; import android.widget.TextView; -import androidx.annotation.Nullable; - import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.PhoneWindow; import com.android.settingslib.R; @@ -80,7 +77,6 @@ public class EnableDndDialogFactory { private static final int SECONDS_MS = 1000; private static final int MINUTES_MS = 60 * SECONDS_MS; - @Nullable private final EnableDndDialogMetricsLogger mMetricsLogger; @VisibleForTesting @@ -152,16 +148,10 @@ public class EnableDndDialogFactory { Slog.d(TAG, "Invalid manual condition: " + tag.condition); } // always triggers priority-only dnd with chosen condition - if (Flags.modesApi()) { - mNotificationManager.setZenMode( - Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, - getRealConditionId(tag.condition), TAG, - /* fromUser= */ true); - } else { - mNotificationManager.setZenMode( - Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, - getRealConditionId(tag.condition), TAG); - } + mNotificationManager.setZenMode( + Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, + getRealConditionId(tag.condition), TAG, + /* fromUser= */ true); } }); diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java index 6e64c597f5cc..34e08af18f93 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java @@ -34,6 +34,8 @@ import com.android.settingslib.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import androidx.annotation.NonNull; + /** * This class is used to create custom dialog with icon, title, message and custom view that are * horizontally centered. @@ -191,20 +193,52 @@ public class CustomDialogHelper { } /** + * Sets title of the dialog by string. + */ + @NonNull public CustomDialogHelper setTitle(@NonNull CharSequence title) { + mDialogTitle.setText(title); + return this; + } + + /** + * Sets title padding of the dialog. + */ + @NonNull public CustomDialogHelper setTitlePadding(int left, int top, int right, int bottom) { + mDialogTitle.setPadding(left, top, right, bottom); + return this; + } + + /** * Sets message of the dialog. */ - public CustomDialogHelper setMessage(@StringRes int resid) { + @NonNull public CustomDialogHelper setMessage(@StringRes int resid) { mDialogMessage.setText(resid); return this; } /** + * Sets message of the dialog by string. + */ + @NonNull public CustomDialogHelper setMessage(@NonNull CharSequence message) { + mDialogMessage.setText(message); + return this; + } + + /** * Sets message padding of the dialog. */ - public CustomDialogHelper setMessagePadding(int dp) { + @NonNull public CustomDialogHelper setMessagePadding(int dp) { mDialogMessage.setPadding(dp, dp, dp, dp); return this; } + /** + * Sets message padding of the dialog. + */ + @NonNull + public CustomDialogHelper setMessagePadding(int left, int top, int right, int bottom) { + mDialogMessage.setPadding(left, top, right, bottom); + return this; + } /** * Sets icon of the dialog. @@ -215,6 +249,15 @@ public class CustomDialogHelper { } /** + * Sets icon padding of the dialog. + */ + @NonNull + public CustomDialogHelper setIconPadding(int left, int top, int right, int bottom) { + mDialogIcon.setPadding(left, top, right, bottom); + return this; + } + + /** * Removes all views that were previously added to the custom layout part. */ public CustomDialogHelper clearCustomLayout() { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index d933a1ced8bc..f6e26a7200ef 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -17,6 +17,7 @@ package com.android.settingslib.bluetooth; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; import static com.android.settingslib.flags.Flags.FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE; +import static com.android.settingslib.flags.Flags.FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI; import static com.google.common.truth.Truth.assertThat; @@ -78,11 +79,14 @@ public class CachedBluetoothDeviceTest { private static final String TWS_BATTERY_RIGHT = "25"; private static final String TWS_LOW_BATTERY_THRESHOLD_LOW = "10"; private static final String TWS_LOW_BATTERY_THRESHOLD_HIGH = "25"; + private static final String TEMP_BOND_METADATA = + "<TEMP_BOND_TYPE>le_audio_sharing</TEMP_BOND_TYPE>"; private static final short RSSI_1 = 10; private static final short RSSI_2 = 11; private static final boolean JUSTDISCOVERED_1 = true; private static final boolean JUSTDISCOVERED_2 = false; private static final int LOW_BATTERY_COLOR = android.R.color.holo_red_dark; + private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25; @Mock private LocalBluetoothProfileManager mProfileManager; @Mock @@ -128,6 +132,7 @@ public class CachedBluetoothDeviceTest { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TV_MEDIA_OUTPUT_DIALOG); mSetFlagsRule.enableFlags(FLAG_ENABLE_SET_PREFERRED_TRANSPORT_FOR_LE_AUDIO_DEVICE); mSetFlagsRule.enableFlags(FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(FLAG_ENABLE_TEMPORARY_BOND_DEVICES_UI); mContext = RuntimeEnvironment.application; mAudioManager = mContext.getSystemService(AudioManager.class); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); @@ -2075,6 +2080,87 @@ public class CachedBluetoothDeviceTest { } @Test + public void getConnectionSummary_GuestDeviceBroadcastPrimary_activeDevice_returnActive() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + Settings.Secure.putInt( + mContext.getContentResolver(), + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); + + List<Long> bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(any())).thenReturn(sourceList); + + when(mCachedDevice.getGroupId()).thenReturn(1); + when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true); + + assertThat(mCachedDevice.getConnectionSummary(false)) + .isEqualTo(mContext.getString(R.string.bluetooth_active_no_battery_level)); + } + + @Test + public void getConnectionSummary_GuestDeviceBroadcastSecondary_activeDevice_returnGuestMedia() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + Settings.Secure.putInt( + mContext.getContentResolver(), + BluetoothUtils.getPrimaryGroupIdUriForBroadcast(), + 1); + when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); + + List<Long> bisSyncState = new ArrayList<>(); + bisSyncState.add(1L); + when(mLeBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState); + List<BluetoothLeBroadcastReceiveState> sourceList = new ArrayList<>(); + sourceList.add(mLeBroadcastReceiveState); + when(mAssistant.getAllSources(any())).thenReturn(sourceList); + + when(mCachedDevice.getGroupId()).thenReturn(BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + + assertThat(mCachedDevice.getConnectionSummary(false)) + .isEqualTo( + mContext.getString(R.string.bluetooth_guest_media_only_no_battery_level)); + } + + @Test + public void getConnectionSummary_GuestDeviceSupportsBroadcastConnected_returnGuestSupportLe() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true); + when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); + + when(mCachedDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); + when(mCachedDevice.isConnected()).thenReturn(true); + + assertThat(mCachedDevice.getConnectionSummary(false)) + .isEqualTo( + mContext.getString(R.string.bluetooth_guest_no_battery_level_lea_support)); + } + + @Test + public void getConnectionSummary_GuestDeviceSupportsBroadcastNotConnected_returnSavedGuest() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true); + when(mDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + .thenReturn(TEMP_BOND_METADATA.getBytes()); + + when(mCachedDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile)); + when(mCachedDevice.isConnected()).thenReturn(false); + + assertThat(mCachedDevice.getConnectionSummary(false)) + .isEqualTo(mContext.getString(R.string.bluetooth_guest_saved_device_lea_support)); + } + + @Test public void isHearingDevice_supportHearingRelatedProfiles_returnTrue() { when(mCachedDevice.getProfiles()).thenReturn( ImmutableList.of(mHapClientProfile, mHearingAidProfile)); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt index 388af61c6273..b364368df473 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/data/repository/ZenModeRepositoryTest.kt @@ -91,7 +91,6 @@ class ZenModeRepositoryTest { ) } - @EnableFlags(android.app.Flags.FLAG_MODES_API) @Test fun consolidatedPolicyChanges_repositoryEmits_flagsOn() { testScope.runTest { @@ -110,7 +109,6 @@ class ZenModeRepositoryTest { } } - @EnableFlags(android.app.Flags.FLAG_MODES_API) @Test fun consolidatedPolicyChanges_repositoryEmitsFromExtras() { testScope.runTest { diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 236654deefb5..f5c0233d56b1 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -8,7 +8,6 @@ achalke@google.com acul@google.com adamcohen@google.com aioana@google.com -alexchau@google.com alexflo@google.com andonian@google.com amiko@google.com @@ -91,10 +90,8 @@ rahulbanerjee@google.com rgl@google.com roosa@google.com saff@google.com -samcackett@google.com santie@google.com shanh@google.com -silvajordan@google.com snoeberger@google.com spdonghao@google.com steell@google.com @@ -106,7 +103,6 @@ thiruram@google.com tracyzhou@google.com tsuji@google.com twickham@google.com -uwaisashraf@google.com vadimt@google.com valiiftime@google.com vanjan@google.com @@ -121,3 +117,11 @@ yuandizhou@google.com yurilin@google.com yuzhechen@google.com zakcohen@google.com + +# Overview eng team +alexchau@google.com +samcackett@google.com +silvajordan@google.com +uwaisashraf@google.com +vinayjoglekar@google.com +willosborn@google.com diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp index 088ec136f24e..f5bff859269f 100644 --- a/packages/SystemUI/aconfig/Android.bp +++ b/packages/SystemUI/aconfig/Android.bp @@ -28,6 +28,7 @@ package { "//frameworks/libs/systemui/tracinglib:__subpackages__", "//frameworks/base/services/accessibility:__subpackages__", "//frameworks/base/services/tests:__subpackages__", + "//packages/apps/Settings:__subpackages__", "//platform_testing:__subpackages__", "//vendor:__subpackages__", "//cts:__subpackages__", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 2c3a5eacf940..29b578ae6e48 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -67,6 +67,13 @@ flag { } flag { + name: "notifications_redesign_guts" + namespace: "systemui" + description: "Notifications Redesign: Update the look of the notification guts (that appear on long press). This includes using the new cache for app icons." + bug: "394822197" +} + +flag { name: "notification_row_content_binder_refactor" namespace: "systemui" description: "Convert the NotificationContentInflater to Kotlin and restructure it to support modern views" @@ -967,6 +974,26 @@ flag { } flag { + name: "use_notif_inflation_thread_for_footer" + namespace: "systemui" + description: "use the @NotifInflation thread for FooterView and EmptyShadeView inflation" + bug: "375320642" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "use_notif_inflation_thread_for_row" + namespace: "systemui" + description: "use the @NotifInflation thread for ExpandableNotificationRow inflation" + bug: "375320642" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notify_power_manager_user_activity_background" namespace: "systemui" description: "Decide whether to notify the user activity to power manager in the background thread." @@ -1970,6 +1997,16 @@ flag { } flag { + name: "hardware_color_styles" + namespace: "systemui" + description: "Enables loading initial colors based ion hardware color" + bug: "347286986" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "shade_launch_accessibility" namespace: "systemui" description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events." @@ -1980,6 +2017,16 @@ flag { } flag { + name: "expand_collapse_privacy_dialog" + namespace: "systemui" + description: "Add expand and collapse actions to accessibility, to allow announcement in TalkBack when state changes." + bug: "380161221" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "show_locked_by_your_watch_keyguard_indicator" namespace: "systemui" description: "Show a Locked by your watch indicator on the keyguard when the device is locked by the watch." @@ -1999,3 +2046,23 @@ flag { description: "Enables the clock fidget animation" bug: "364664389" } + +flag { + name: "notifications_launch_radius" + namespace: "systemui" + description: "Fixes a discrepancy in corner radius between expanding notification and opening window during launch animations." + bug: "396054791" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "skip_hide_sensitive_notif_animation" + namespace: "systemui" + description: "Skip hide sensitive notification animation when the showing layout is not changed." + bug: "390624334" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt index 65cd3c79cd16..444389fb26ea 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt @@ -36,6 +36,7 @@ import android.view.ViewGroupOverlay import android.widget.FrameLayout import com.android.internal.jank.Cuj.CujType import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import java.util.LinkedList import kotlin.math.min import kotlin.math.roundToInt @@ -58,7 +59,7 @@ open class GhostedViewTransitionAnimatorController @JvmOverloads constructor( /** The view that will be ghosted and from which the background will be extracted. */ - private val ghostedView: View, + transitioningView: View, /** The [CujType] associated to this launch animation. */ private val launchCujType: Int? = null, @@ -75,11 +76,24 @@ constructor( private val isEphemeral: Boolean = false, private var interactionJankMonitor: InteractionJankMonitor = InteractionJankMonitor.getInstance(), + + /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */ + private val transitionRegistry: IViewTransitionRegistry? = + if (Flags.decoupleViewControllerInAnimlib()) { + ViewTransitionRegistry.instance + } else { + null + } ) : ActivityTransitionAnimator.Controller { override val isLaunching: Boolean = true /** The container to which we will add the ghost view and expanding background. */ - override var transitionContainer = ghostedView.rootView as ViewGroup + override var transitionContainer: ViewGroup + get() = ghostedView.rootView as ViewGroup + set(_) { + // empty, should never be set to avoid memory leak + } + private val transitionContainerOverlay: ViewGroupOverlay get() = transitionContainer.overlay @@ -138,9 +152,33 @@ constructor( } } + /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */ + private val transitionToken = + if (Flags.decoupleViewControllerInAnimlib()) { + ViewTransitionToken(transitioningView::class.java) + } else { + null + } + + /** The view that will be ghosted and from which the background will be extracted */ + private val ghostedView: View + get() = + if (Flags.decoupleViewControllerInAnimlib()) { + transitionRegistry?.getView(transitionToken!!) + } else { + _ghostedView + }!! + + private val _ghostedView = + if (Flags.decoupleViewControllerInAnimlib()) { + null + } else { + transitioningView + } + init { // Make sure the View we launch from implements LaunchableView to avoid visibility issues. - if (ghostedView !is LaunchableView) { + if (transitioningView !is LaunchableView) { throw IllegalArgumentException( "A GhostedViewLaunchAnimatorController was created from a View that does not " + "implement LaunchableView. This can lead to subtle bugs where the visibility " + @@ -148,6 +186,10 @@ constructor( ) } + if (Flags.decoupleViewControllerInAnimlib()) { + transitionRegistry?.register(transitionToken!!, transitioningView) + } + /** Find the first view with a background in [view] and its children. */ fun findBackground(view: View): Drawable? { if (view.background != null) { @@ -184,6 +226,7 @@ constructor( if (TransitionAnimator.returnAnimationsEnabled()) { ghostedView.removeOnAttachStateChangeListener(detachListener) } + transitionToken?.let { token -> transitionRegistry?.unregister(token) } } /** @@ -237,7 +280,7 @@ constructor( val insets = backgroundInsets val boundCorrections: Rect = if (ghostedView is LaunchableView) { - ghostedView.getPaddingForLaunchAnimation() + (ghostedView as LaunchableView).getPaddingForLaunchAnimation() } else { Rect() } @@ -387,8 +430,8 @@ constructor( if (ghostedView is LaunchableView) { // Restore the ghosted view visibility. - ghostedView.setShouldBlockVisibilityChanges(false) - ghostedView.onActivityLaunchAnimationEnd() + (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false) + (ghostedView as LaunchableView).onActivityLaunchAnimationEnd() } else { // Make the ghosted view visible. We ensure that the view is considered VISIBLE by // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17 @@ -398,7 +441,7 @@ constructor( ghostedView.invalidate() } - if (isEphemeral) { + if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) { onDispose() } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt new file mode 100644 index 000000000000..af3ca87bf788 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 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.animation + +import android.view.View + +/** Represents a Registry for holding a transitioning view mapped to a token */ +interface IViewTransitionRegistry { + + /** + * Registers the transitioning [view] mapped to a [token] + * + * @param token The token corresponding to the transitioning view + * @param view The view undergoing transition + */ + fun register(token: ViewTransitionToken, view: View) + + /** + * Unregisters the transitioned view from its corresponding [token] + * + * @param token The token corresponding to the transitioning view + */ + fun unregister(token: ViewTransitionToken) + + /** + * Extracts a transitioning view from registry using its corresponding [token] + * + * @param token The token corresponding to the transitioning view + */ + fun getView(token: ViewTransitionToken): View? + + /** + * Return token mapped to the [view], if it is present in the registry + * + * @param view the transitioning view whose token we are requesting + * @return token associated with the [view] if present, else null + */ + fun getViewToken(view: View): ViewTransitionToken? + + /** Event call to run on registry update (on both [register] and [unregister]) */ + fun onRegistryUpdate() +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt index b9f9bc7e2daa..5b073e49192a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt @@ -27,9 +27,8 @@ import android.graphics.fonts.FontVariationAxis import android.text.Layout import android.util.Log import android.util.LruCache - -private const val DEFAULT_ANIMATION_DURATION: Long = 300 -private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 +import androidx.annotation.VisibleForTesting +import com.android.app.animation.Interpolators typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit @@ -76,6 +75,10 @@ class TypefaceVariantCacheImpl(var baseTypeface: Typeface, override val animatio cache.put(fvar, it) } } + + companion object { + private const val TYPEFACE_CACHE_MAX_ENTRIES = 5 + } } /** @@ -108,25 +111,12 @@ class TextAnimator( private val typefaceCache: TypefaceVariantCache, private val invalidateCallback: () -> Unit = {}, ) { - // Following two members are for mutable for testing purposes. - public var textInterpolator = TextInterpolator(layout, typefaceCache) - public var animator = - ValueAnimator.ofFloat(1f).apply { - duration = DEFAULT_ANIMATION_DURATION - addUpdateListener { - textInterpolator.progress = it.animatedValue as Float - textInterpolator.linearProgress = - it.currentPlayTime.toFloat() / it.duration.toFloat() - invalidateCallback() - } - addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) = textInterpolator.rebase() + @VisibleForTesting var textInterpolator = TextInterpolator(layout, typefaceCache) + @VisibleForTesting var createAnimator: () -> ValueAnimator = { ValueAnimator.ofFloat(1f) } - override fun onAnimationCancel(animation: Animator) = textInterpolator.rebase() - } - ) - } + var animator: ValueAnimator? = null + + val fontVariationUtils = FontVariationUtils() sealed class PositionedGlyph { /** Mutable X coordinate of the glyph position relative from drawing offset. */ @@ -165,8 +155,6 @@ class TextAnimator( protected set } - private val fontVariationUtils = FontVariationUtils() - fun updateLayout(layout: Layout, textSize: Float = -1f) { textInterpolator.layout = layout @@ -178,9 +166,8 @@ class TextAnimator( } } - fun isRunning(): Boolean { - return animator.isRunning - } + val isRunning: Boolean + get() = animator?.isRunning ?: false /** * GlyphFilter applied just before drawing to canvas for tweaking positions and text size. @@ -237,110 +224,110 @@ class TextAnimator( fun draw(c: Canvas) = textInterpolator.draw(c) - /** - * Set text style with animation. - * - * ``` - * By passing -1 to weight, the view preserve the current weight. - * By passing -1 to textSize, the view preserve the current text size. - * By passing -1 to duration, the default text animation, 1000ms, is used. - * By passing false to animate, the text will be updated without animation. - * ``` - * - * @param fvar an optional text fontVariationSettings. - * @param textSize an optional font size. - * @param colors an optional colors array that must be the same size as numLines passed to the - * TextInterpolator - * @param strokeWidth an optional paint stroke width - * @param animate an optional boolean indicating true for showing style transition as animation, - * false for immediate style transition. True by default. - * @param duration an optional animation duration in milliseconds. This is ignored if animate is - * false. - * @param interpolator an optional time interpolator. If null is passed, last set interpolator - * will be used. This is ignored if animate is false. - */ - fun setTextStyle( - fvar: String? = "", - textSize: Float = -1f, - color: Int? = null, - strokeWidth: Float = -1f, - animate: Boolean = true, - duration: Long = -1L, - interpolator: TimeInterpolator? = null, - delay: Long = 0, - onAnimationEnd: Runnable? = null, + /** Style spec to use when rendering the font */ + data class Style( + val fVar: String? = null, + val textSize: Float? = null, + val color: Int? = null, + val strokeWidth: Float? = null, ) { - setTextStyleInternal( - fvar, - textSize, - color, - strokeWidth, - animate, - duration, - interpolator, - delay, - onAnimationEnd, - updateLayoutOnFailure = true, - ) + fun withUpdatedFVar( + fontVariationUtils: FontVariationUtils, + weight: Int = -1, + width: Int = -1, + opticalSize: Int = -1, + roundness: Int = -1, + ): Style { + return this.copy( + fVar = + fontVariationUtils.updateFontVariation( + weight = weight, + width = width, + opticalSize = opticalSize, + roundness = roundness, + ) + ) + } } - private fun setTextStyleInternal( - fvar: String?, - textSize: Float, - color: Int?, - strokeWidth: Float, - animate: Boolean, - duration: Long, - interpolator: TimeInterpolator?, - delay: Long, - onAnimationEnd: Runnable?, - updateLayoutOnFailure: Boolean, + /** Animation Spec for use when style changes should be animated */ + data class Animation( + val animate: Boolean = true, + val startDelay: Long = 0, + val duration: Long = DEFAULT_ANIMATION_DURATION, + val interpolator: TimeInterpolator = Interpolators.LINEAR, + val onAnimationEnd: Runnable? = null, ) { - try { - if (animate) { - animator.cancel() - textInterpolator.rebase() + fun configureAnimator(animator: Animator) { + animator.startDelay = startDelay + animator.duration = duration + animator.interpolator = interpolator + if (onAnimationEnd != null) { + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onAnimationEnd.run() + } + } + ) } + } - if (textSize >= 0) { - textInterpolator.targetPaint.textSize = textSize - } - if (!fvar.isNullOrBlank()) { - textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(fvar) - } - if (color != null) { - textInterpolator.targetPaint.color = color - } - if (strokeWidth >= 0F) { - textInterpolator.targetPaint.strokeWidth = strokeWidth + companion object { + val DISABLED = Animation(animate = false) + } + } + + /** Sets the text style, optionally with animation */ + fun setTextStyle(style: Style, animation: Animation = Animation.DISABLED) { + animator?.cancel() + setTextStyleInternal(style, rebase = animation.animate) + + if (animation.animate) { + animator = buildAnimator(animation).apply { start() } + } else { + textInterpolator.progress = 1f + textInterpolator.rebase() + invalidateCallback() + } + } + + /** Builds a ValueAnimator from the specified animation parameters */ + private fun buildAnimator(animation: Animation): ValueAnimator { + return createAnimator().apply { + duration = DEFAULT_ANIMATION_DURATION + animation.configureAnimator(this) + + addUpdateListener { + textInterpolator.progress = it.animatedValue as Float + textInterpolator.linearProgress = it.currentPlayTime / it.duration.toFloat() + invalidateCallback() } - textInterpolator.onTargetPaintModified() - if (animate) { - animator.startDelay = delay - animator.duration = if (duration == -1L) DEFAULT_ANIMATION_DURATION else duration - interpolator?.let { animator.interpolator = it } - if (onAnimationEnd != null) { - animator.addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - onAnimationEnd.run() - animator.removeListener(this) - } - - override fun onAnimationCancel(animation: Animator) { - animator.removeListener(this) - } - } - ) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) = textInterpolator.rebase() + + override fun onAnimationCancel(animator: Animator) = textInterpolator.rebase() } - animator.start() - } else { - // No animation is requested, thus set base and target state to the same state. - textInterpolator.progress = 1f - textInterpolator.rebase() - invalidateCallback() + ) + } + } + + private fun setTextStyleInternal( + style: Style, + rebase: Boolean, + updateLayoutOnFailure: Boolean = true, + ) { + try { + if (rebase) textInterpolator.rebase() + style.color?.let { textInterpolator.targetPaint.color = it } + style.textSize?.let { textInterpolator.targetPaint.textSize = it } + style.strokeWidth?.let { textInterpolator.targetPaint.strokeWidth = it } + style.fVar?.let { + textInterpolator.targetPaint.typeface = typefaceCache.getTypefaceForVariant(it) } + textInterpolator.onTargetPaintModified() } catch (ex: IllegalArgumentException) { if (updateLayoutOnFailure) { Log.e( @@ -351,81 +338,15 @@ class TextAnimator( ) updateLayout(textInterpolator.layout) - setTextStyleInternal( - fvar, - textSize, - color, - strokeWidth, - animate, - duration, - interpolator, - delay, - onAnimationEnd, - updateLayoutOnFailure = false, - ) + setTextStyleInternal(style, rebase, updateLayoutOnFailure = false) } else { throw ex } } } - /** - * Set text style with animation. Similar as - * - * ``` - * fun setTextStyle( - * fvar: String? = "", - * textSize: Float = -1f, - * color: Int? = null, - * strokeWidth: Float = -1f, - * animate: Boolean = true, - * duration: Long = -1L, - * interpolator: TimeInterpolator? = null, - * delay: Long = 0, - * onAnimationEnd: Runnable? = null - * ) - * ``` - * - * @param weight an optional style value for `wght` in fontVariationSettings. - * @param width an optional style value for `wdth` in fontVariationSettings. - * @param opticalSize an optional style value for `opsz` in fontVariationSettings. - * @param roundness an optional style value for `ROND` in fontVariationSettings. - */ - fun setTextStyle( - weight: Int = -1, - width: Int = -1, - opticalSize: Int = -1, - roundness: Int = -1, - textSize: Float = -1f, - color: Int? = null, - strokeWidth: Float = -1f, - animate: Boolean = true, - duration: Long = -1L, - interpolator: TimeInterpolator? = null, - delay: Long = 0, - onAnimationEnd: Runnable? = null, - ) { - setTextStyleInternal( - fvar = - fontVariationUtils.updateFontVariation( - weight = weight, - width = width, - opticalSize = opticalSize, - roundness = roundness, - ), - textSize = textSize, - color = color, - strokeWidth = strokeWidth, - animate = animate, - duration = duration, - interpolator = interpolator, - delay = delay, - onAnimationEnd = onAnimationEnd, - updateLayoutOnFailure = true, - ) - } - companion object { private val TAG = TextAnimator::class.simpleName!! + const val DEFAULT_ANIMATION_DURATION = 300L } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt index 58c2a1c98ec4..86c7f76c6bee 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt @@ -24,14 +24,14 @@ import java.lang.ref.WeakReference * A registry to temporarily store the view being transitioned into a Dialog (using * [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator]) */ -class ViewTransitionRegistry { +class ViewTransitionRegistry : IViewTransitionRegistry { /** * A map of a unique token to a WeakReference of the View being transitioned. WeakReference * ensures that Views are garbage collected whenever they become eligible and avoid any * memory leaks */ - private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() } + private val registry by lazy { mutableMapOf<ViewTransitionToken, WeakReference<View>>() } /** * A [View.OnAttachStateChangeListener] to be attached to all views stored in the registry to @@ -45,8 +45,7 @@ class ViewTransitionRegistry { } override fun onViewDetachedFromWindow(view: View) { - (view.getTag(R.id.tag_view_transition_token) - as? ViewTransitionToken)?.let { token -> unregister(token) } + getViewToken(view)?.let { token -> unregister(token) } } } } @@ -57,12 +56,12 @@ class ViewTransitionRegistry { * @param token unique token associated with the transitioning view * @param view view undergoing transitions */ - fun register(token: ViewTransitionToken, view: View) { + override fun register(token: ViewTransitionToken, view: View) { // token embedded as a view tag enables to use a single listener for all views view.setTag(R.id.tag_view_transition_token, token) view.addOnAttachStateChangeListener(listener) registry[token] = WeakReference(view) - emitCountForTrace() + onRegistryUpdate() } /** @@ -70,30 +69,51 @@ class ViewTransitionRegistry { * * @param token unique token associated with the transitioning view */ - fun unregister(token: ViewTransitionToken) { + override fun unregister(token: ViewTransitionToken) { registry.remove(token)?.let { it.get()?.let { view -> view.removeOnAttachStateChangeListener(listener) view.setTag(R.id.tag_view_transition_token, null) } it.clear() + onRegistryUpdate() } - emitCountForTrace() } /** * Access a view from registry using unique "token" associated with it * WARNING - this returns a StrongReference to the View stored in the registry */ - fun getView(token: ViewTransitionToken): View? { + override fun getView(token: ViewTransitionToken): View? { return registry[token]?.get() } /** + * Return token mapped to the [view], if it is present in the registry + * + * @param view the transitioning view whose token we are requesting + * @return token associated with the [view] if present, else null + */ + override fun getViewToken(view: View): ViewTransitionToken? { + return (view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken)?.let { token -> + getView(token)?.let { token } + } + } + + /** Event call to run on registry update (on both [register] and [unregister]) */ + override fun onRegistryUpdate() { + emitCountForTrace() + } + + /** * Utility function to emit number of non-null views in the registry whenever the registry is * updated (via [register] or [unregister]) */ private fun emitCountForTrace() { Trace.setCounter("transition_registry_view_count", registry.count().toLong()) } + + companion object { + val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ViewTransitionRegistry() } + } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt index c211a8ed1de2..e011df01504f 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt @@ -16,17 +16,19 @@ package com.android.systemui.animation +import java.util.UUID + /** * A token uniquely mapped to a View in [ViewTransitionRegistry]. This token is guaranteed to be * unique as timestamp is appended to the token string * - * @constructor creates an instance of [ViewTransitionToken] with token as "timestamp" or - * "ClassName_timestamp" + * @constructor creates an instance of [ViewTransitionToken] with token as "UUID" or + * "ClassName_UUID" * * @property token String value of a unique token */ @JvmInline value class ViewTransitionToken private constructor(val token: String) { - constructor() : this(token = System.currentTimeMillis().toString()) - constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${System.currentTimeMillis()}") + constructor() : this(token = UUID.randomUUID().toString()) + constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${UUID.randomUUID()}") } 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 ba85f9570d09..5806458da9b5 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 @@ -20,11 +20,8 @@ import android.view.View import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.internal.jank.Cuj import com.android.internal.jank.Cuj.CujType @@ -70,8 +67,7 @@ class LockscreenContent( rememberViewModel("LockscreenContent-scrimViewModel") { notificationScrimViewModelFactory.create() } - val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle() - if (!isContentVisible) { + if (!viewModel.isContentVisible) { // If the content isn't supposed to be visible, show a large empty box as it's needed // for scene transition animations (can't just skip rendering everything or shared // elements won't have correct final/initial bounds from animating in and out of the @@ -80,15 +76,13 @@ class LockscreenContent( return } - val coroutineScope = rememberCoroutineScope() - val blueprintId by viewModel.blueprintId(coroutineScope).collectAsStateWithLifecycle() DisposableEffect(view) { clockInteractor.clockEventController.registerListeners(view) onDispose { clockInteractor.clockEventController.unregisterListeners() } } - val blueprint = blueprintByBlueprintId[blueprintId] ?: return + val blueprint = blueprintByBlueprintId[viewModel.blueprintId] ?: return with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) NotificationLockscreenScrim(notificationLockscreenScrimViewModel) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index d2fff06ad746..590a74ee2a0d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -32,7 +31,6 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.modifiers.padding import com.android.systemui.compose.modifiers.sysuiResTag @@ -45,6 +43,8 @@ import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.composable.section.TopAreaSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BelowClock +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BesideClock import com.android.systemui.res.R import java.util.Optional import javax.inject.Inject @@ -71,11 +71,8 @@ constructor( @Composable override fun ContentScope.Content(viewModel: LockscreenContentViewModel, modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible - val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle() - val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle() - val areNotificationsVisible by - viewModel.areNotificationsVisible().collectAsStateWithLifecycle(initialValue = false) - val isBypassEnabled by viewModel.isBypassEnabled.collectAsStateWithLifecycle() + val isBypassEnabled = viewModel.isBypassEnabled + val notificationsPlacement = viewModel.notificationsPlacement if (isBypassEnabled) { with(notificationSection) { HeadsUpNotifications() } @@ -92,7 +89,9 @@ constructor( modifier = Modifier.fillMaxWidth() .padding( - horizontal = { unfoldTranslations.start.roundToInt() } + horizontal = { + viewModel.unfoldTranslations.start.roundToInt() + } ) ) } @@ -101,28 +100,28 @@ constructor( with(topAreaSection) { DefaultClockLayout( smartSpacePaddingTop = viewModel::getSmartSpacePaddingTop, - isShadeLayoutWide = isShadeLayoutWide, modifier = Modifier.fillMaxWidth().graphicsLayer { - translationX = unfoldTranslations.start + translationX = viewModel.unfoldTranslations.start }, ) } - if (isShadeLayoutWide && !isBypassEnabled) { + if (notificationsPlacement is BesideClock && !isBypassEnabled) { with(notificationSection) { Box(modifier = Modifier.fillMaxHeight()) { AodPromotedNotificationArea( modifier = Modifier.fillMaxWidth(0.5f) - .align(alignment = Alignment.TopEnd) + .align(notificationsPlacement.alignment) ) Notifications( - areNotificationsVisible = areNotificationsVisible, + areNotificationsVisible = + viewModel.areNotificationsVisible, burnInParams = null, modifier = Modifier.fillMaxWidth(0.5f) .fillMaxHeight() - .align(alignment = Alignment.TopEnd) + .align(notificationsPlacement.alignment) .padding(top = 12.dp), ) } @@ -138,7 +137,7 @@ constructor( dimensionResource(R.dimen.below_clock_padding_start_icons) with(notificationSection) { - if (!isShadeLayoutWide && !isBypassEnabled) { + if (notificationsPlacement is BelowClock && !isBypassEnabled) { Box(modifier = Modifier.weight(weight = 1f)) { Column(Modifier.align(alignment = Alignment.TopStart)) { AodPromotedNotificationArea( @@ -150,13 +149,13 @@ constructor( ) } Notifications( - areNotificationsVisible = areNotificationsVisible, + areNotificationsVisible = viewModel.areNotificationsVisible, burnInParams = null, ) } } else { Column { - if (!isShadeLayoutWide) { + if (viewModel.notificationsPlacement is BelowClock) { AodPromotedNotificationArea( modifier = Modifier.padding(top = aodPromotedNotifTopPadding) @@ -204,13 +203,17 @@ constructor( isStart = true, applyPadding = true, modifier = - Modifier.graphicsLayer { translationX = unfoldTranslations.start }, + Modifier.graphicsLayer { + translationX = viewModel.unfoldTranslations.start + }, ) Shortcut( isStart = false, applyPadding = true, modifier = - Modifier.graphicsLayer { translationX = unfoldTranslations.end }, + Modifier.graphicsLayer { + translationX = viewModel.unfoldTranslations.end + }, ) } with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt index d8b3f742b447..0876631cf5c1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt @@ -41,16 +41,13 @@ constructor( ) { @Composable - fun ContentScope.KeyguardMediaCarousel( - isShadeLayoutWide: Boolean, - modifier: Modifier = Modifier, - ) { + fun ContentScope.KeyguardMediaCarousel(modifier: Modifier = Modifier) { val viewModel = rememberViewModel(traceName = "KeyguardMediaCarousel") { keyguardMediaViewModelFactory.create() } val horizontalPadding = - if (isShadeLayoutWide) { + if (viewModel.isShadeLayoutWide) { dimensionResource(id = R.dimen.notification_side_paddings) } else { dimensionResource(id = R.dimen.notification_side_paddings) + diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 6293fc26f96a..013424006668 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -60,7 +60,6 @@ constructor( @Composable fun ContentScope.DefaultClockLayout( smartSpacePaddingTop: (Resources) -> Int, - isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, ) { val currentClockLayout by clockViewModel.currentClockLayout.collectAsStateWithLifecycle() @@ -128,7 +127,7 @@ constructor( ) } } - with(mediaCarouselSection) { KeyguardMediaCarousel(isShadeLayoutWide) } + with(mediaCarouselSection) { KeyguardMediaCarousel() } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 64f3cb13662a..297995becfb2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -23,7 +23,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.dimensionResource import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey @@ -34,6 +34,13 @@ import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.media.controls.ui.composable.MediaCarousel +import com.android.systemui.media.controls.ui.composable.isLandscape +import com.android.systemui.media.controls.ui.controller.MediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaHost +import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED +import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED +import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel import com.android.systemui.res.R @@ -42,10 +49,11 @@ import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.OverlayShadeHeader -import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView +import com.android.systemui.util.Utils import dagger.Lazy import javax.inject.Inject +import javax.inject.Named import kotlinx.coroutines.flow.Flow @SysUISingleton @@ -58,6 +66,8 @@ constructor( private val stackScrollView: Lazy<NotificationScrollView>, private val clockSection: DefaultClockSection, private val keyguardClockViewModel: KeyguardClockViewModel, + private val mediaCarouselController: MediaCarouselController, + @Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>, ) : Overlay { override val key = Overlays.NotificationsShade @@ -84,6 +94,11 @@ constructor( viewModel.notificationsPlaceholderViewModelFactory.create() } + val usingCollapsedLandscapeMedia = + Utils.useCollapsedMediaInLandscape(LocalResources.current) + mediaHost.get().expansion = + if (usingCollapsedLandscapeMedia && isLandscape()) COLLAPSED else EXPANDED + OverlayShade( panelElement = NotificationsShade.Elements.Panel, alignmentOnWideScreens = Alignment.TopStart, @@ -96,9 +111,7 @@ constructor( } OverlayShadeHeader( viewModel = headerViewModel, - modifier = - Modifier.element(NotificationsShade.Elements.StatusBar) - .layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader), + modifier = Modifier.element(NotificationsShade.Elements.StatusBar), ) }, ) { @@ -116,6 +129,19 @@ constructor( } } + MediaCarousel( + isVisible = viewModel.showMedia, + mediaHost = mediaHost.get(), + carouselController = mediaCarouselController, + usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia, + modifier = + Modifier.padding( + top = notificationStackPadding, + start = notificationStackPadding, + end = notificationStackPadding, + ), + ) + NotificationScrollingStack( shadeSession = shadeSession, stackScrollView = stackScrollView.get(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt index 8aa5bc7b7c6f..60eaa28e3822 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt @@ -21,6 +21,7 @@ import com.android.compose.animation.scene.MutableSceneTransitionLayoutState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.TransitionKey +import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.observableTransitionState import com.android.systemui.scene.shared.model.SceneDataSource import kotlinx.coroutines.CoroutineScope @@ -103,4 +104,8 @@ class SceneTransitionLayoutDataSource( override fun instantlyHideOverlay(overlay: OverlayKey) { state.snapTo(overlays = state.currentOverlays - overlay) } + + override fun freezeAndAnimateToCurrentState() { + (state.transitionState as? TransitionState.Transition)?.freezeAndAnimateToCurrentState() + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index b76656d78cc4..4bf0ceb51784 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -366,7 +366,7 @@ constructor( fun animateCharge(isDozing: () -> Boolean) { // Skip charge animation if dozing animation is already playing. - if (textAnimator == null || textAnimator!!.isRunning()) { + if (textAnimator == null || textAnimator!!.isRunning) { return } @@ -444,29 +444,28 @@ constructor( delay: Long, onAnimationEnd: Runnable?, ) { - textAnimator?.let { - it.setTextStyle( - weight = weight, - color = color, + val style = TextAnimator.Style(color = color) + val animation = + TextAnimator.Animation( animate = animate && isAnimationEnabled, duration = duration, - interpolator = interpolator, - delay = delay, + interpolator = interpolator ?: Interpolators.LINEAR, + startDelay = delay, onAnimationEnd = onAnimationEnd, ) + textAnimator?.let { + it.setTextStyle( + style.withUpdatedFVar(it.fontVariationUtils, weight = weight), + animation, + ) it.glyphFilter = glyphFilter } ?: run { // when the text animator is set, update its start values onTextAnimatorInitialized = { textAnimator -> textAnimator.setTextStyle( - weight = weight, - color = color, - animate = false, - duration = duration, - interpolator = interpolator, - delay = delay, - onAnimationEnd = onAnimationEnd, + style.withUpdatedFVar(textAnimator.fontVariationUtils, weight = weight), + animation.copy(animate = false), ) textAnimator.glyphFilter = glyphFilter } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index a5adfa2a1ac6..0b7ea1a335ef 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -21,6 +21,7 @@ import android.view.ViewGroup import android.view.animation.Interpolator import android.widget.RelativeLayout import androidx.annotation.VisibleForTesting +import com.android.systemui.animation.TextAnimator import com.android.systemui.customization.R import com.android.systemui.log.core.Logger import com.android.systemui.plugins.clocks.AlarmData @@ -65,7 +66,7 @@ data class DigitalAlignment( data class FontTextStyle( val lineHeight: Float? = null, val fontSizeScale: Float? = null, - val transitionDuration: Long = -1L, + val transitionDuration: Long = TextAnimator.DEFAULT_ANIMATION_DURATION, val transitionInterpolator: Interpolator? = null, ) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 92fa6b5be1ed..8317aa39ef2b 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -33,7 +33,9 @@ import android.util.MathUtils import android.util.TypedValue import android.view.View.MeasureSpec.EXACTLY import android.view.animation.Interpolator +import android.view.animation.PathInterpolator import android.widget.TextView +import com.android.app.animation.Interpolators import com.android.internal.annotations.VisibleForTesting import com.android.systemui.animation.GSFAxes import com.android.systemui.animation.TextAnimator @@ -84,17 +86,20 @@ open class SimpleDigitalClockTextView( else -> listOf(FLEX_AOD_SMALL_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS) } - private var lsFontVariation = - if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS).toFVar() - else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS).toFVar() + private var lsFontVariation: String + private var aodFontVariation: String + private var fidgetFontVariation: String - private var aodFontVariation = run { + init { val roundAxis = if (!isLegacyFlex) ROUND_AXIS else FLEX_ROUND_AXIS - (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar() - } + val lsFontAxes = + if (!isLegacyFlex) listOf(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS) + else listOf(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS) - // TODO(b/374306512): Fidget endpoint to spec - private var fidgetFontVariation = aodFontVariation + lsFontVariation = lsFontAxes.toFVar() + aodFontVariation = (fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar() + fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar() + } private val parser = DimensionParser(clockCtx.context) var maxSingleDigitHeight = -1 @@ -121,7 +126,7 @@ open class SimpleDigitalClockTextView( protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!) get() = field ?: ClockLogger.INIT_LOGGER - private var aodDozingInterpolator: Interpolator? = null + private var aodDozingInterpolator: Interpolator = Interpolators.LINEAR @VisibleForTesting lateinit var textAnimator: TextAnimator @@ -149,7 +154,7 @@ open class SimpleDigitalClockTextView( lockscreenColor = color lockScreenPaint.color = lockscreenColor if (dozeFraction < 1f) { - textAnimator.setTextStyle(color = lockscreenColor, animate = false) + textAnimator.setTextStyle(TextAnimator.Style(color = lockscreenColor)) } invalidate() } @@ -157,10 +162,8 @@ open class SimpleDigitalClockTextView( fun updateAxes(lsAxes: List<ClockFontAxisSetting>) { lsFontVariation = lsAxes.toFVar() aodFontVariation = lsAxes.replace(fixedAodAxes).toFVar() - logger.i({ "updateAxes(LS = $str1, AOD = $str2)" }) { - str1 = lsFontVariation - str2 = aodFontVariation - } + fidgetFontVariation = buildFidgetVariation(lsAxes).toFVar() + logger.updateAxes(lsFontVariation, aodFontVariation) lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) typeface = lockScreenPaint.typeface @@ -168,13 +171,28 @@ open class SimpleDigitalClockTextView( lockScreenPaint.getTextBounds(text, 0, text.length, textBounds) targetTextBounds.set(textBounds) - textAnimator.setTextStyle(fvar = lsFontVariation, animate = false) + textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation)) measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) recomputeMaxSingleDigitSizes() requestLayout() invalidate() } + fun buildFidgetVariation(axes: List<ClockFontAxisSetting>): List<ClockFontAxisSetting> { + val result = mutableListOf<ClockFontAxisSetting>() + for (axis in axes) { + result.add( + FIDGET_DISTS.get(axis.key)?.let { (dist, midpoint) -> + ClockFontAxisSetting( + axis.key, + axis.value + dist * if (axis.value > midpoint) -1 else 1, + ) + } ?: axis + ) + } + return result + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { logger.onMeasure() super.onMeasure(widthMeasureSpec, heightMeasureSpec) @@ -245,40 +263,54 @@ open class SimpleDigitalClockTextView( fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { if (!this::textAnimator.isInitialized) return + logger.animateDoze() textAnimator.setTextStyle( - animate = isAnimated && isAnimationEnabled, - color = if (isDozing) AOD_COLOR else lockscreenColor, - textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, - fvar = if (isDozing) aodFontVariation else lsFontVariation, - duration = aodStyle.transitionDuration, - interpolator = aodDozingInterpolator, + TextAnimator.Style( + fVar = if (isDozing) aodFontVariation else lsFontVariation, + color = if (isDozing) AOD_COLOR else lockscreenColor, + textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, + ), + TextAnimator.Animation( + animate = isAnimated && isAnimationEnabled, + duration = aodStyle.transitionDuration, + interpolator = aodDozingInterpolator, + ), ) updateTextBoundsForTextAnimator() } fun animateCharge() { - if (!this::textAnimator.isInitialized || textAnimator.isRunning()) { + if (!this::textAnimator.isInitialized || textAnimator.isRunning) { // Skip charge animation if dozing animation is already playing. return } - logger.d("animateCharge()") + logger.animateCharge() + + val lsStyle = TextAnimator.Style(fVar = lsFontVariation) + val aodStyle = TextAnimator.Style(fVar = aodFontVariation) + textAnimator.setTextStyle( - fvar = if (dozeFraction == 0F) aodFontVariation else lsFontVariation, - animate = isAnimationEnabled, - onAnimationEnd = - Runnable { + if (dozeFraction == 0f) aodStyle else lsStyle, + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = CHARGE_ANIMATION_DURATION, + onAnimationEnd = { textAnimator.setTextStyle( - fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation, - animate = isAnimationEnabled, + if (dozeFraction == 0f) lsStyle else aodStyle, + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = CHARGE_ANIMATION_DURATION, + ), ) updateTextBoundsForTextAnimator() }, + ), ) updateTextBoundsForTextAnimator() } fun animateFidget(x: Float, y: Float) { - if (!this::textAnimator.isInitialized || textAnimator.isRunning()) { + if (!this::textAnimator.isInitialized || textAnimator.isRunning) { // Skip fidget animation if other animation is already playing. return } @@ -286,19 +318,25 @@ open class SimpleDigitalClockTextView( logger.animateFidget(x, y) clockCtx.vibrator?.vibrate(FIDGET_HAPTICS) - // TODO(b/374306512): Duplicated charge animation as placeholder. Implement final version - // when we have a complete spec. May require additional code to animate individual digits. + // TODO(b/374306512): Delay each glyph's animation based on x/y position textAnimator.setTextStyle( - fvar = fidgetFontVariation, - animate = isAnimationEnabled, - onAnimationEnd = - Runnable { + TextAnimator.Style(fVar = fidgetFontVariation), + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = FIDGET_ANIMATION_DURATION, + interpolator = FIDGET_INTERPOLATOR, + onAnimationEnd = { textAnimator.setTextStyle( - fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation, - animate = isAnimationEnabled, + TextAnimator.Style(fVar = lsFontVariation), + TextAnimator.Animation( + animate = isAnimationEnabled, + duration = FIDGET_ANIMATION_DURATION, + interpolator = FIDGET_INTERPOLATOR, + ), ) updateTextBoundsForTextAnimator() }, + ), ) updateTextBoundsForTextAnimator() } @@ -329,42 +367,20 @@ open class SimpleDigitalClockTextView( } private fun getInterpolatedTextBounds(): Rect { - val interpolatedTextBounds = Rect() - if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) { - interpolatedTextBounds.left = - MathUtils.lerp( - prevTextBounds.left, - targetTextBounds.left, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - - interpolatedTextBounds.right = - MathUtils.lerp( - prevTextBounds.right, - targetTextBounds.right, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - - interpolatedTextBounds.top = - MathUtils.lerp( - prevTextBounds.top, - targetTextBounds.top, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - - interpolatedTextBounds.bottom = - MathUtils.lerp( - prevTextBounds.bottom, - targetTextBounds.bottom, - textAnimator.animator.animatedValue as Float, - ) - .toInt() - } else { - interpolatedTextBounds.set(targetTextBounds) + val progress = textAnimator.animator?.let { it.animatedValue as Float } ?: 1f + if (!textAnimator.isRunning || progress >= 1f) { + return Rect(targetTextBounds) } + + val interpolatedTextBounds = Rect() + interpolatedTextBounds.left = + MathUtils.lerp(prevTextBounds.left, targetTextBounds.left, progress).toInt() + interpolatedTextBounds.right = + MathUtils.lerp(prevTextBounds.right, targetTextBounds.right, progress).toInt() + interpolatedTextBounds.top = + MathUtils.lerp(prevTextBounds.top, targetTextBounds.top, progress).toInt() + interpolatedTextBounds.bottom = + MathUtils.lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress).toInt() return interpolatedTextBounds } @@ -471,7 +487,7 @@ open class SimpleDigitalClockTextView( textStyle.lineHeight?.let { lineHeight = it.toInt() } this.aodStyle = aodStyle ?: textStyle.copy() - this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it } + aodDozingInterpolator = this.aodStyle.transitionInterpolator ?: Interpolators.LINEAR lockScreenPaint.strokeWidth = textBorderWidth measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) setInterpolatorPaint() @@ -500,7 +516,7 @@ open class SimpleDigitalClockTextView( recomputeMaxSingleDigitSizes() if (this::textAnimator.isInitialized) { - textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false) + textAnimator.setTextStyle(TextAnimator.Style(textSize = lockScreenPaint.textSize)) } } @@ -525,10 +541,11 @@ open class SimpleDigitalClockTextView( textAnimator.textInterpolator.targetPaint.set(lockScreenPaint) textAnimator.textInterpolator.onTargetPaintModified() textAnimator.setTextStyle( - fvar = lsFontVariation, - textSize = lockScreenPaint.textSize, - color = lockscreenColor, - animate = false, + TextAnimator.Style( + fVar = lsFontVariation, + textSize = lockScreenPaint.textSize, + color = lockscreenColor, + ) ) } } @@ -572,6 +589,17 @@ open class SimpleDigitalClockTextView( .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43) .compose() + val CHARGE_ANIMATION_DURATION = 500L + val FIDGET_ANIMATION_DURATION = 250L + val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f) + val FIDGET_DISTS = + mapOf( + GSFAxes.WEIGHT to Pair(200f, 500f), + GSFAxes.WIDTH to Pair(30f, 75f), + GSFAxes.ROUND to Pair(0f, 50f), + GSFAxes.SLANT to Pair(0f, -5f), + ) + val AOD_COLOR = Color.WHITE val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f) val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f) diff --git a/packages/SystemUI/docs/qs-tiles.md b/packages/SystemUI/docs/qs-tiles.md index ee388ec8e5c5..87d9b7c3b853 100644 --- a/packages/SystemUI/docs/qs-tiles.md +++ b/packages/SystemUI/docs/qs-tiles.md @@ -8,6 +8,14 @@ This document is a more or less comprehensive summary of the state and infrastru Settings tiles. It provides descriptions about the lifecycle of a tile, how to create new tiles and how SystemUI manages and displays tiles, among other topics. +A lot of the tile backend architecture is in the process of being replaced by a new architecture in +order to align with the +[recommended architecture](https://developer.android.com/topic/architecture#recommended-app-arch). + +While we are in the process of migrating, this document will try to provide a comprehensive +overview of the current architecture as well as the new one. The sections documenting the new +architecture are marked with the tag [NEW-ARCH]. + ## What are Quick Settings Tiles? Quick Settings (from now on, QS) is the expanded panel that contains shortcuts for the user to @@ -72,6 +80,27 @@ The interfaces in `QSTile` as well as other interfaces described in this documen implement plugins to add additional tiles or different behavior. For more information, see [plugins.md](plugins.md) + +#### [NEW-ARCH] Tile backend +Instead of `QSTileImpl` the tile backend is made of a view model called `QSTileViewModelImpl`, +which in turn is composed of 3 interfaces: + +* [`QSTileDataInteractor`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileDataInteractor.kt) +is responsible for providing the data for the tile. It is responsible for fetching the state of +the tile from the source of truth and providing that information to the tile. Typically the data +interactor will read system state from a repository or a controller and provide a flow of +domain-specific data model. + +* [`QSTileUserActionInteractor`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt) is responsible for handling the user actions on the tile. +This interactor decides what should happen when the user clicks, long clicks on the tile. + +* [`QSTileDataToStateMapper`](/packages/SystemUI/src/com/android/systemui/qs/tiles/base/mapper/QSTileMapper.kt) +is responsible for mapping the data received from the data interactor to a state that the view +model can use to update the UI. + +At the time being, the `QSTileViewModel`s are adapted to `QSTile`. This conversion is done by +`QSTileViewModelAdapter`. + #### Tile State Each tile has an associated `State` object that is used to communicate information to the @@ -94,6 +123,11 @@ Additionally. `BooleanState` has a `value` boolean field that usually would be s to `state == Tile#STATE_ACTIVE`. This is used by accessibility services along with `expandedAccessibilityClassName`. +#### [NEW-ARCH] Tile State +In the new architecture, the mapper generates +[`QSTileState`](packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt), +which again is converted to the old state by `QSTileViewModelAdapter`. + #### SystemUI tiles Each tile defined in SystemUI extends `QSTileImpl`. This abstract class implements some common @@ -103,6 +137,9 @@ to handle different events (refresh, click, etc.). For more information on how to implement a tile in SystemUI, see [Implementing a SystemUI tile](#implementing-a-systemui-tile). +As mentioned before, when the [NEW-ARCH] migration is complete, we will remove the `QSTileImpl` +and `QSTileViewModelAdapter` and directly use`QSTileViewModelImpl`. + ### Tile views Each Tile has a couple of associated views for displaying it in QS and QQS. These views are updated @@ -154,37 +191,24 @@ corresponding `QSTile` with its `QSTileView`, doing the following: #### Life of a tile click This is a brief run-down of what happens when a user clicks on a tile. Internal changes on the -device (for example, changes from Settings) will trigger this process starting in step 3. Throughout -this section, we assume that we are dealing with a `QSTileImpl`. - -1. User clicks on tile. The following calls happen in sequence: - 1. `QSTileViewImpl#onClickListener`. - 2. `QSTile#click`. - 3. `QSTileImpl#handleClick`. This last call sets the new state for the device by using the - associated controller. -2. State in the device changes. This is normally outside of SystemUI's control. -3. Controller receives a callback (or `Intent`) indicating the change in the device. The following - calls happen: - 1. `QSTileImpl#refreshState`, maybe passing an object with necessary information regarding the - new state. - 2. `QSTileImpl#handleRefreshState` -4. `QSTileImpl#handleUpdateState` is called to update the state with the new information. This - information can be obtained both from the `Object` passed to `refreshState` as well as from the - controller. -5. If the state has changed (in at least one element), `QSTileImpl#handleStateChanged` is called. - This will trigger a call to all the associated `QSTile.Callback#onStateChanged`, passing the - new `State`. -6. `QSTileView#onStateChanged` is called and this calls `QSTileView#handleStateChanged`. This method - maps the state into the view: - * The tile colors change to match the new state. - * `QSIconView.setIcon` is called to apply the correct state to the icon and the correct icon to - the view. - * The tile labels change to match the new state. +device (for example, changes from Settings) will trigger this process starting in step 5. + +Step | Legacy Tiles | [NEW-ARCH] Tiles +-------|-------|--------- +1 | User clicks on tile. | Same as legacy tiles. +2 | `QSTileViewImpl#onClickListener` | Same as legacy tiles. +3 | `QSTile#click` | Same as legacy tiles. +4| `QSTileImpl#handleClick` | `QSTileUserActionInteractor#handleInput` +5| State in the device changes. This is normally outside of SystemUI's control. Controller receives a callback (or `Intent`) indicating the change in the device. | Same as legacy tiles. +6 | `QSTile#refreshState`and `QSTileImpl#handleRefreshState` | `QSTileDataInteractor#tileData()` +7| `QSTileImpl#handleUpdateState` is called to update the state with the new information. This information can be obtained both from the `Object` passed to `refreshState` as well as from the controller. | The data that was generated by the data interactor is read by the `QSTileViewModelImpl.state` flow which calls `QSTileMapper#map` on the data to generate a new `QSTileState`. +8| If the state has changed (in at least one element `QSTileImpl#handleStateChanged` is called. This will trigger a call to all the associated `QSTile.Callback#onStateChanged`, passing the new `State`. | The newly mapped QSTileState is read by the `QSTileViewModelAdapter` which then maps it to a legacy `State`. Similarly to the legacy tiles, the new state is compared to the old one and if there is a difference, `QSTile.Callback#onStateChanged` is called for all the associated callbacks. +9 | `QSTileView#onStateChanged` is called and this calls `QSTileView#handleStateChanged`. This method maps the state updating tile color and label, and calling `QSIconView.setIcon` | Same as legacy tiles. ## Third party tiles (TileService) -A third party tile is any Quick Settings tile that is provided by an app (that's not SystemUI). This -is implemented by developers +A third party tile is any Quick Settings tile that is provided by an app (that's not SystemUI). +This is implemented by developers subclassing [`TileService`](/core/java/android/service/quicksettings/TileService.java) and interacting with its API. @@ -220,9 +244,9 @@ from SystemUI: * **`onTileAdded`**: called when the tile is added to QS. * **`onTileRemoved`**: called when the tile is removed from QS. * **`onStartListening`**: called when QS is opened and the tile is showing. This marks the start of - the window when calling `getQSTile` is safe and will provide the correct object. -* **`onStopListening`**: called when QS is closed or the tile is no longer visible by the user. This - marks the end of the window described in `onStartListening`. +the window when calling `getQSTile` is safe and will provide the correct object. +* **`onStopListening`**: called when QS is closed or the tile is no longer visible by the user. +This marks the end of the window described in `onStartListening`. * **`onClick`**: called when the user clicks on the tile. Additionally, the following final methods are provided: @@ -379,13 +403,14 @@ correct list of tiles. ### QSFactory +`CurrentTilesInteractorImpl` uses the `QSFactory` interface to create the tiles. + This interface provides a way of creating tiles and views from a spec. It can be used in plugins to provide different definitions for tiles. -In SystemUI there is only one implementation of this factory and that is the default -factory (`QSFactoryImpl`) in `CurrentTilesInteractorImpl`. +In SystemUI there are two implementation of this factory. The first one is `QSFactoryImpl` in used for legacy tiles. The second one is `NewQSFactory` used for [NEW-ARCH] tiles. -#### QSFactoryImpl +#### QSFactoryImpl (legacy tiles) This class implements the following method as specified in the `QSFactory` interface: @@ -402,6 +427,12 @@ This class implements the following method as specified in the `QSFactory` inter As part of filtering not valid tiles, custom tiles that don't have a corresponding valid service component are never instantiated. +#### NewQSFactory ([NEW-ARCH] tiles) + +This class also implements the `createTile` method as specified in the `QSFactory` interface. +However, it first uses the spec to get a `QSTileViewModel`. The view model is then adapted into a +`QSTile` using the `QSTileViewModelAdapter`. + ### Lifecycle of a Tile We describe first the parts of the lifecycle that are common to SystemUI tiles and third party @@ -415,7 +446,7 @@ tiles. 2. This updates the flow that `CurrentTilesInteractor` is collecting from, triggering the process described above. 3. `CurrentTilesInteractor` calls the available `QSFactory` classes in order to find one that will - be able to create a tile with that spec. Assuming that `QSFactoryImpl` managed to create the + be able to create a tile with that spec. Assuming that some factory managed to create the tile, which is some implementation of `QSTile` (either a SystemUI subclass of `QSTileImpl` or a `CustomTile`) it will be added to the current list. If the tile is available, it's stored in a map and things proceed forward. @@ -452,7 +483,7 @@ in [Third party tiles (TileService)](#third-party-tiles-tileservice). This section describes necessary and recommended steps when implementing a Quick Settings tile. Some of them are optional and depend on the requirements of the tile. -### Implementing a SystemUI tile +### Implementing a legacy SystemUI tile 1. Create a class (preferably in [`SystemUI/src/com/android/systemui/qs/tiles`](/packages/SystemUI/src/com/android/systemui/qs/tiles)) @@ -579,6 +610,70 @@ type variable of type `State`. Provides a default label for this Tile. Used by the QS Panel customizer to show a name next to each available tile. +### Implementing a [NEW-ARCH] SystemUI tile +In the new system the tiles are created in the path +[`packages/SystemUI/src/com/android/systemui/qs/tiles/impl/<spec>`](packages/SystemUI/src/com/android/systemui/qs/tiles/impl/<spec>) +where the `<spec>` should be replaced by the spec of the tile e.g. rotation for `RotationLockTile`. + +To create a new tile, the developer needs to implement the following data class and interfaces: + +[`DataModel`] is a class that describes the system state of the feature that the tile is trying to +represent. Let's refer to the type of this class as DATA_TYPE. For example a simplified version of +the data model for a flashlight tile could be a class with a boolean field that represents +whether the flashlight is on or not. + +This file should be placed in the relative path `domain/model/` down from the tile's package. + +[`QSTileDataInteractor`] There are two abstract methods that need to be implemented: +* `fun tileData(user: UserHandle, triggers: Flow<DataUpdateTrigger>): Flow<DATA_TYPE>`: This method +returns a flow of data that will be used to create the state of the tile. This is where the system +state is listened to and converted to a flow of data model. Avoid loading data or settings up +listeners on the main thread. The userHandle is the user for which the tile is created. +The triggers flow is a flow of events that can be used to trigger a refresh of the data. +The most common triggers the force update and initial request. + +* `fun availability(user: UserHandle): Flow<Boolean>`: This method returns a flow of booleans that +indicates if the tile should be shown or not. This is where the availability of the system feature +(e.g. wifi) is checked. The userHandle is the user for which the tile is created. + +This file should be placed in the relative path `domain/interactor/` down from the tile's package. + +[`QSTileUserActionInteractor`] +* `fun handleInput(input: QSTileInput)` is the method that needs to be implemented. This is the +method that will be called when the user interacts with the tile. The input parameter contains +the type of interaction (click, long click, toggle) and the DATA_TYPE of the latest data when the +input was received. + +This file should be placed in the relative path `/domain/interactor` down from the tile's package. + +[`QSTileDataToStateMapper`] +* `fun map(data: DATA_TYPE): QSTileState` is the method that needs to be implemented. This method +is responsible for mapping the data received from the data interactor to a state that the view +model can use to update the UI. This is where for example the icon should be loaded, and the +label and content description set. The map function will run on UIBackground thread, a single +thread which has higher priority than the background thread and lower than UI thread. Loading a +resource on UI thread can cause jank by blocking the UI thread. On the other end of the spectrum, +loading resources using a background dispatcher may cause jank due to background thread contention +since it is possible for the background dispatcher to use more than one background thread +at the same time. In contrast, the UIBackground dispatcher uses a single thread that is +shared by all tiles. Therefore the system will use UIBackground dispatcher to execute +the map function. + +The most important resource to load in the map function is the icon. We prefer `Icon.Loaded` with a +resource id over `Icon.Resource`, because then (a) we can guarantee that the drawable loading will +happen on the UIBackground thread and (b) we can cache the drawables using the resource id. + +This file should be placed in the relative path `/ui/mapper` down from the tile's package. + +#### Testing a [NEW-ARCH] SystemUI tile + +When writing tests, the mapper is usually a good place to start, since that is where the +business logic decisions are being made that can inform the shape of data interactor. + +We suggest taking advantage of the existing class `QSTileStateSubject`. So rather than +asserting an individual field's value, a test will assert the whole state. That can be +achieved by `QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)`. + ### Implementing a third party tile For information about this, use the Android Developer documentation diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java index bd811814eb24..4140a956182c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardMessageAreaControllerTest.java @@ -18,9 +18,7 @@ package com.android.keyguard; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -28,8 +26,6 @@ import static org.mockito.Mockito.when; import android.hardware.biometrics.BiometricSourceType; import android.testing.TestableLooper; -import android.text.Editable; -import android.text.TextWatcher; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -99,19 +95,6 @@ public class KeyguardMessageAreaControllerTest extends SysuiTestCase { } @Test - public void textChanged_AnnounceForAccessibility() { - ArgumentCaptor<TextWatcher> textWatcherArgumentCaptor = ArgumentCaptor.forClass( - TextWatcher.class); - mMessageAreaController.onViewAttached(); - verify(mKeyguardMessageArea).addTextChangedListener(textWatcherArgumentCaptor.capture()); - - textWatcherArgumentCaptor.getValue().afterTextChanged( - Editable.Factory.getInstance().newEditable("abc")); - verify(mKeyguardMessageArea).removeCallbacks(any(Runnable.class)); - verify(mKeyguardMessageArea).postDelayed(any(Runnable.class), anyLong()); - } - - @Test public void testSetBouncerVisible() { mMessageAreaController.setIsVisible(true); verify(mKeyguardMessageArea).setIsVisible(true); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt index d6ba98d65d15..441f807a8ec8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.core.Logger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -136,6 +137,118 @@ class ActivityManagerRepositoryTest : SysuiTestCase() { assertThat(latest).isFalse() } + @Test + fun createAppVisibilityFlow_fetchesInitialValue_trueWithLastVisibleTime() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND) + fakeSystemClock.setCurrentTimeMillis(5000) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest!!.isAppCurrentlyVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(5000) + } + + @Test + fun createAppVisibilityFlow_fetchesInitialValue_falseWithoutLastVisibleTime() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + fakeSystemClock.setCurrentTimeMillis(5000) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() + } + + @Test + fun createAppVisibilityFlow_getsImportanceUpdates_updatesLastVisibleTimeOnlyWhenVisible() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + fakeSystemClock.setCurrentTimeMillis(5000) + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() + + val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() + verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) + val listener = listenerCaptor.firstValue + + // WHEN the app becomes visible + fakeSystemClock.setCurrentTimeMillis(7000) + listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) + + // THEN the status and lastAppVisibleTime are updated + assertThat(latest!!.isAppCurrentlyVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) + + // WHEN the app is no longer visible + listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING) + + // THEN the lastAppVisibleTime is preserved + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(7000) + + // WHEN the app is visible again + fakeSystemClock.setCurrentTimeMillis(9000) + listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) + + // THEN the lastAppVisibleTime is updated + assertThat(latest!!.isAppCurrentlyVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) + } + + @Test + fun createAppVisibilityFlow_ignoresUpdatesForOtherUids() = + kosmos.runTest { + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() + verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) + val listener = listenerCaptor.firstValue + + listener.onUidImportance(THIS_UID, IMPORTANCE_GONE) + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + + // WHEN another UID becomes foreground + listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND) + + // THEN this UID still stays not visible + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + } + + @Test + fun createAppVisibilityFlow_securityExceptionOnUidRegistration_ok() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + whenever(activityManager.addOnUidImportanceListener(any(), any())) + .thenThrow(SecurityException()) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + // Verify no crash, and we get a value emitted + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + } + + /** Regression test for b/216248574. */ + @Test + fun createAppVisibilityFlow_getUidImportanceThrowsException_ok() = + kosmos.runTest { + whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException()) + + val latest by + collectLastValue(underTest.createAppVisibilityFlow(THIS_UID, logger, LOG_TAG)) + + // Verify no crash, and we get a value emitted + assertThat(latest!!.isAppCurrentlyVisible).isFalse() + } + companion object { private const val THIS_UID = 558 private const val LOG_TAG = "LogTag" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt index e492c63d095c..052d520ac92f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt @@ -17,16 +17,20 @@ package com.android.systemui.animation import android.os.HandlerThread +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.view.View import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.view.LaunchableFrameLayout import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -40,6 +44,14 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { } private val interactionJankMonitor = FakeInteractionJankMonitor() + private lateinit var transitionRegistry: FakeViewTransitionRegistry + private lateinit var transitioningView: View + + @Before + fun setup() { + transitioningView = LaunchableFrameLayout(mContext) + transitionRegistry = FakeViewTransitionRegistry() + } @Test fun animatingOrphanViewDoesNotCrash() { @@ -67,7 +79,7 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { parent.addView((launchView)) val launchController = GhostedViewTransitionAnimatorController( - launchView, + launchView, launchCujType = LAUNCH_CUJ, returnCujType = RETURN_CUJ, interactionJankMonitor = interactionJankMonitor @@ -96,6 +108,26 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ) } + @EnableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB) + @Test + fun testViewsAreRegisteredInTransitionRegistry() { + GhostedViewTransitionAnimatorController( + transitioningView = transitioningView, + transitionRegistry = transitionRegistry + ) + assertThat(transitionRegistry.registry).isNotEmpty() + } + + @DisableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB) + @Test + fun testNotUseRegistryIfDecouplingFlagDisabled() { + GhostedViewTransitionAnimatorController( + transitioningView = transitioningView, + transitionRegistry = transitionRegistry + ) + assertThat(transitionRegistry.registry).isEmpty() + } + /** * A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and * allows inspection. @@ -117,4 +149,30 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() { return true } } + + private class FakeViewTransitionRegistry : IViewTransitionRegistry { + + val registry = mutableMapOf<ViewTransitionToken, View>() + + override fun register(token: ViewTransitionToken, view: View) { + registry[token] = view + view.setTag(R.id.tag_view_transition_token, token) + } + + override fun unregister(token: ViewTransitionToken) { + registry.remove(token)?.setTag(R.id.tag_view_transition_token, null) + } + + override fun getView(token: ViewTransitionToken): View? { + return registry[token] + } + + override fun getViewToken(view: View): ViewTransitionToken? { + return view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken + } + + override fun onRegistryUpdate() { + //empty + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java index 01baadda7c87..c40c1a3b0e93 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsViewTest.java @@ -130,12 +130,17 @@ public class SeekBarWithIconButtonsViewTest extends SysuiTestCase { @Test public void setProgress_onProgressChangedAndOnUserInteractionFinalized() { reset(mOnSeekBarChangeListener); - mIconDiscreteSliderLinearLayout.setProgress(1); + + // Trigger the progress changed listener with fromUser but without clicking. + // This is similar to what would happen if an accessibility service changed the + // progress. + mIconDiscreteSliderLinearLayout.getSeekBarChangeListener().onProgressChanged( + mIconDiscreteSliderLinearLayout.getSeekbar(), 1, /*fromUser=*/ true); // If users are changing seekbar progress without touching the seekbar or clicking the // buttons, trigger onUserInteractionFinalized. verify(mOnSeekBarChangeListener).onProgressChanged( - eq(mSeekbar), /* progress= */ eq(1), /* fromUser= */ eq(false)); + eq(mSeekbar), /* progress= */ eq(1), /* fromUser= */ eq(true)); verify(mOnSeekBarChangeListener, never()).onStartTrackingTouch(/* seekBar= */ any()); verify(mOnSeekBarChangeListener, never()).onStopTrackingTouch(/* seekBar= */ any()); verify(mOnSeekBarChangeListener).onUserInteractionFinalized( @@ -144,6 +149,22 @@ public class SeekBarWithIconButtonsViewTest extends SysuiTestCase { } @Test + public void setProgress_onProgressChangedWithoutUserInteractionFinalized() { + reset(mOnSeekBarChangeListener); + mIconDiscreteSliderLinearLayout.setProgress(1); + + // If seekbar progress changes due to a non-user event, without touching the seekbar or + // clicking the buttons, do not trigger onUserInteractionFinalized. + verify(mOnSeekBarChangeListener).onProgressChanged( + eq(mSeekbar), /* progress= */ eq(1), /* fromUser= */ eq(false)); + verify(mOnSeekBarChangeListener, never()).onStartTrackingTouch(/* seekBar= */ any()); + verify(mOnSeekBarChangeListener, never()).onStopTrackingTouch(/* seekBar= */ any()); + verify(mOnSeekBarChangeListener, never()).onUserInteractionFinalized( + /* seekBar= */ any(), + eq(OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER)); + } + + @Test public void setProgressToSeekBarByTouch_onUserInteractionFinalizedAfterTouchEnds() { reset(mOnSeekBarChangeListener); final SeekBarWithIconButtonsView.SeekBarChangeListener seekBarChangeListener = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt index 1a3606e413cc..da25bcac6c95 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -44,6 +45,7 @@ import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -65,7 +67,7 @@ class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestC private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val communalSceneRepository = kosmos.fakeCommunalSceneRepository - private val sceneInteractor = kosmos.sceneInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val underTest: CommunalTransitionViewModel by lazy { kosmos.communalTransitionViewModel diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index 329627af8ec2..e36d2455d316 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -61,6 +61,7 @@ import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.eq 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 @@ -73,6 +74,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.never import org.mockito.Mockito.verify +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { @@ -80,21 +82,26 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { private val testScope: TestScope = kosmos.testScope private lateinit var underTest: SystemUIDeviceEntryFaceAuthInteractor + private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository private val fakeUserRepository = kosmos.fakeUserRepository private val facePropertyRepository = kosmos.facePropertyRepository - private val fakeDeviceEntryFingerprintAuthInteractor = - kosmos.deviceEntryFingerprintAuthInteractor - private val powerInteractor = kosmos.powerInteractor private val fakeBiometricSettingsRepository = kosmos.fakeBiometricSettingsRepository - private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor + private val keyguardUpdateMonitor by lazy { kosmos.keyguardUpdateMonitor } private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig private val trustManager = kosmos.trustManager - private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor + + private val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor } + private val fakeDeviceEntryFingerprintAuthInteractor by lazy { + kosmos.deviceEntryFingerprintAuthInteractor + } + private val powerInteractor by lazy { kosmos.powerInteractor } + private val deviceEntryFaceAuthStatusInteractor by lazy { + kosmos.deviceEntryFaceAuthStatusInteractor + } @Before fun setup() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 183e4d6f624b..98486a22854a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -541,7 +541,7 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), - simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"), + simpleShortcutCategory(MultiTasking, "Split screen", "Use full screen"), simpleShortcutCategory( MultiTasking, "Split screen", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt index 9b80ca303cd3..63229dbb47a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt @@ -229,6 +229,39 @@ class FromAlternateBouncerTransitionInteractorTest(flags: FlagsParameterization) } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun transitionToDreaming() = + kosmos.runTest { + fakePowerRepository.updateWakefulness( + WakefulnessState.AWAKE, + WakeSleepReason.POWER_BUTTON, + WakeSleepReason.POWER_BUTTON, + false, + ) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeKeyguardBouncerRepository.setAlternateVisible(true) + runCurrent() + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.ALTERNATE_BOUNCER, + testScope, + ) + reset(transitionRepository) + + fakeKeyguardRepository.setKeyguardOccluded(true) + fakeKeyguardRepository.setDreaming(true) + fakeKeyguardBouncerRepository.setAlternateVisible(false) + testScope.advanceTimeBy(200) // advance past delay + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.DREAMING, + ) + } + + @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun transitionToGone_whenOpeningGlanceableHubEditMode() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt index 282bebcd629a..a08c0dea6fa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.shared.model.ClockSize +import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState @@ -68,6 +69,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockSize_sceneContainerFlagOff_basedOnRepository() = testScope.runTest { val value by collectLastValue(underTest.clockSize) + kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.DYNAMIC) kosmos.keyguardClockRepository.setClockSize(ClockSize.LARGE) assertThat(value).isEqualTo(ClockSize.LARGE) @@ -76,6 +78,17 @@ class KeyguardClockInteractorTest : SysuiTestCase() { } @Test + @DisableSceneContainer + fun clockSize_sceneContainerFlagOff_smallClockSettingSelected_SMALL() = + testScope.runTest { + val value by collectLastValue(underTest.clockSize) + kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL) + kosmos.keyguardClockRepository.setClockSize(ClockSize.LARGE) + + assertThat(value).isEqualTo(ClockSize.SMALL) + } + + @Test @EnableSceneContainer fun clockSize_forceSmallClock_SMALL() = testScope.runTest { @@ -91,61 +104,80 @@ class KeyguardClockInteractorTest : SysuiTestCase() { @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasNotifs_SMALL() = + fun clockSize_sceneContainerFlagOn_shadeModeSingle_hasNotifs_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) kosmos.shadeRepository.setShadeLayoutWide(false) kosmos.activeNotificationListRepository.setActiveNotifs(1) + assertThat(value).isEqualTo(ClockSize.SMALL) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasMedia_SMALL() = + fun clockSize_sceneContainerFlagOn_shadeModeSingle_hasMedia_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) kosmos.shadeRepository.setShadeLayoutWide(false) val userMedia = MediaData().copy(active = true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + assertThat(value).isEqualTo(ClockSize.SMALL) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSplit_isMediaVisible_SMALL() = + fun clockSize_sceneContainerFlagOn_shadeModeSplit_isMediaVisible_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) val userMedia = MediaData().copy(active = true) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) kosmos.keyguardRepository.setIsDozing(false) + assertThat(value).isEqualTo(ClockSize.SMALL) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSplit_noMedia_LARGE() = + fun clockSize_sceneContainerFlagOn_shadeModeSplit_noMedia_LARGE() = testScope.runTest { val value by collectLastValue(underTest.clockSize) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.keyguardRepository.setIsDozing(false) + assertThat(value).isEqualTo(ClockSize.LARGE) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSplit_isDozing_LARGE() = + fun clockSize_sceneContainerFlagOn_shadeModeSplit_isDozing_LARGE() = testScope.runTest { val value by collectLastValue(underTest.clockSize) val userMedia = MediaData().copy(active = true) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) kosmos.keyguardRepository.setIsDozing(true) + assertThat(value).isEqualTo(ClockSize.LARGE) } @Test @EnableSceneContainer + fun clockSize_sceneContainerFlagOn_shadeModeSplit_smallClockSettingSelectd_SMALL() = + testScope.runTest { + val value by collectLastValue(underTest.clockSize) + val userMedia = MediaData().copy(active = true) + kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + kosmos.keyguardRepository.setIsDozing(true) + + assertThat(value).isEqualTo(ClockSize.SMALL) + } + + @Test + @EnableSceneContainer fun clockShouldBeCentered_sceneContainerFlagOn_notSplitMode_true() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) 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 29e95cd911f8..0b42898d82ae 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 @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState @@ -46,20 +47,31 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardTransitionInteractorTest : SysuiTestCase() { - val kosmos = testKosmos() - val underTest = kosmos.keyguardTransitionInteractor - val repository = kosmos.fakeKeyguardTransitionRepository - val testScope = kosmos.testScope + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var repository: FakeKeyguardTransitionRepository + private lateinit var underTest: KeyguardTransitionInteractor + + @Before + fun setup() { + repository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.keyguardTransitionInteractor + } @Test fun transitionCollectorsReceivesOnlyAppropriateEvents() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt index 26fe379f00bf..3cff0fc96af4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt @@ -1358,6 +1358,45 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) } + /** + * When a transition away from the lockscreen is interrupted by an `Idle(Lockscreen)`, a + * `sceneState` that was set during the transition is consumed and passed to KTF. + */ + @Test + fun transition_from_ls_scene_sceneStateSet_then_interrupted_by_idle_on_ls() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = + ObservableTransitionState.Transition( + Scenes.Lockscreen, + Scenes.Gone, + flowOf(Scenes.Lockscreen), + progress, + false, + flowOf(false), + ) + progress.value = 0.4f + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0.4f, + ) + + val sceneState = KeyguardState.AOD + underTest.onSceneAboutToChange(toScene = Scenes.Lockscreen, sceneState = sceneState) + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.AOD, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + private fun assertTransition( step: TransitionStep, from: KeyguardState? = null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt index 3a016ff7152a..63770803ff48 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt @@ -40,7 +40,6 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection -import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.util.mockito.whenever import java.util.Optional import org.junit.Before @@ -66,7 +65,6 @@ class DefaultKeyguardBlueprintTest : SysuiTestCase() { @Mock private lateinit var defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection @Mock private lateinit var defaultStatusBarViewSection: DefaultStatusBarSection @Mock private lateinit var defaultNSSLSection: DefaultNotificationStackScrollLayoutSection - @Mock private lateinit var splitShadeGuidelines: SplitShadeGuidelines @Mock private lateinit var aodPromotedNotificationSection: AodPromotedNotificationSection @Mock private lateinit var aodNotificationIconsSection: AodNotificationIconsSection @Mock private lateinit var aodBurnInSection: AodBurnInSection diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt deleted file mode 100644 index 052dfd52887f..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2025 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.keyguard.ui.viewmodel - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.kosmos.collectValues -import com.android.systemui.kosmos.runTest -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class DozingToDreamingTransitionViewModelTest : SysuiTestCase() { - val kosmos = testKosmos() - - val underTest by lazy { kosmos.dozingToDreamingTransitionViewModel } - - @Test - fun notificationShadeAlpha() = - kosmos.runTest { - val values by collectValues(underTest.notificationAlpha) - assertThat(values).isEmpty() - - fakeKeyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.DOZING, - to = KeyguardState.DREAMING, - testScope, - ) - - assertThat(values).isNotEmpty() - values.forEach { assertThat(it).isEqualTo(0) } - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index 8a599a1bd948..20d015f4d77c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -27,7 +27,6 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.keyguard.shared.model.ClockSize -import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel.ClockLayout import com.android.systemui.kosmos.testScope @@ -55,17 +54,18 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { - val kosmos = testKosmos() - val testScope = kosmos.testScope - val underTest by lazy { kosmos.keyguardClockViewModel } - val res = context.resources - @Mock lateinit var clockController: ClockController - @Mock lateinit var largeClock: ClockFaceController - @Mock lateinit var smallClock: ClockFaceController + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest by lazy { kosmos.keyguardClockViewModel } + private val res = context.resources - var config = ClockConfig("TEST", "Test", "") - var faceConfig = ClockFaceConfig() + @Mock private lateinit var clockController: ClockController + @Mock private lateinit var largeClock: ClockFaceController + @Mock private lateinit var smallClock: ClockFaceController + + private var config = ClockConfig("TEST", "Test", "") + private var faceConfig = ClockFaceConfig() init { mSetFlagsRule.setFlagsParameterization(flags) @@ -196,35 +196,6 @@ class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() } @Test - fun testClockSize_alwaysSmallClockSize() = - testScope.runTest { - val value by collectLastValue(underTest.clockSize) - - with(kosmos) { - fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL) - keyguardClockRepository.setClockSize(ClockSize.LARGE) - } - - assertThat(value).isEqualTo(ClockSize.SMALL) - } - - @Test - @DisableSceneContainer - fun testClockSize_dynamicClockSize() = - testScope.runTest { - with(kosmos) { - val value by collectLastValue(underTest.clockSize) - fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.DYNAMIC) - - keyguardClockRepository.setClockSize(ClockSize.SMALL) - assertThat(value).isEqualTo(ClockSize.SMALL) - - keyguardClockRepository.setClockSize(ClockSize.LARGE) - assertThat(value).isEqualTo(ClockSize.LARGE) - } - } - - @Test fun isLargeClockVisible_whenLargeClockSize_isTrue() = testScope.runTest { val value by collectLastValue(underTest.isLargeClockVisible) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt index 38829da69c28..583fd1e03002 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -81,4 +82,20 @@ class KeyguardMediaViewModelTest : SysuiTestCase() { assertThat(underTest.isMediaVisible).isFalse() } + + @Test + fun isShadeLayoutWide_withConfigTrue_true() = + kosmos.runTest { + shadeRepository.setShadeLayoutWide(true) + + assertThat(underTest.isShadeLayoutWide).isTrue() + } + + @Test + fun isShadeLayoutWide_withConfigFalse_false() = + kosmos.runTest { + shadeRepository.setShadeLayoutWide(false) + + assertThat(underTest.isShadeLayoutWide).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index af025273458f..0b34a01a0fe0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -17,19 +17,24 @@ package com.android.systemui.keyguard.ui.viewmodel import android.platform.test.flag.junit.FlagsParameterization +import androidx.compose.ui.Alignment import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.authController import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.transition.fakeKeyguardTransitionAnimationCallback import com.android.systemui.keyguard.shared.transition.keyguardTransitionAnimationCallbackDelegator +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BelowClock +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BesideClock import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent @@ -41,6 +46,10 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.enableDualShade +import com.android.systemui.shade.domain.interactor.enableSingleShade +import com.android.systemui.shade.domain.interactor.enableSplitShade +import com.android.systemui.shade.domain.interactor.shadeModeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider import com.android.systemui.util.mockito.whenever @@ -99,136 +108,143 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa } @Test - @DisableSceneContainer - fun clockSize_withLargeClock_true() = + fun notificationsPlacement_splitShade_topEnd() = kosmos.runTest { - val clockSize by collectLastValue(underTest.clockSize) - fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) - assertThat(clockSize).isEqualTo(ClockSize.LARGE) + setupState(shadeMode = ShadeMode.Split, clockSize = ClockSize.SMALL) + + assertThat(underTest.notificationsPlacement) + .isEqualTo(BesideClock(alignment = Alignment.TopEnd)) } @Test - @DisableSceneContainer - fun clockSize_withSmallClock_false() = + fun notificationsPlacement_singleShade_below() = kosmos.runTest { - val clockSize by collectLastValue(underTest.clockSize) - fakeKeyguardClockRepository.setClockSize(ClockSize.SMALL) - assertThat(clockSize).isEqualTo(ClockSize.SMALL) + setupState(shadeMode = ShadeMode.Single, clockSize = ClockSize.SMALL) + + assertThat(underTest.notificationsPlacement).isEqualTo(BelowClock) } @Test - fun areNotificationsVisible_splitShadeTrue_true() = + @EnableSceneContainer + fun notificationsPlacement_dualShadeSmallClock_below() = kosmos.runTest { - val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible()) - shadeRepository.setShadeLayoutWide(true) - fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) + setupState( + shadeMode = ShadeMode.Dual, + clockSize = ClockSize.SMALL, + shadeLayoutWide = true, + ) - assertThat(areNotificationsVisible).isTrue() + assertThat(underTest.notificationsPlacement).isEqualTo(BelowClock) } @Test - fun areNotificationsVisible_dualShadeWideOnLockscreen_true() = + @EnableSceneContainer + fun notificationsPlacement_dualShadeLargeClock_topStart() = kosmos.runTest { - val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible()) - kosmos.enableDualShade() - shadeRepository.setShadeLayoutWide(true) - fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) + setupState( + shadeMode = ShadeMode.Dual, + clockSize = ClockSize.LARGE, + shadeLayoutWide = true, + ) - assertThat(areNotificationsVisible).isTrue() + assertThat(underTest.notificationsPlacement) + .isEqualTo(BesideClock(alignment = Alignment.TopStart)) } @Test - @DisableSceneContainer - fun areNotificationsVisible_withSmallClock_true() = + fun areNotificationsVisible_splitShadeTrue_true() = kosmos.runTest { - val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible()) - fakeKeyguardClockRepository.setClockSize(ClockSize.SMALL) - assertThat(areNotificationsVisible).isTrue() + setupState(shadeMode = ShadeMode.Split, clockSize = ClockSize.LARGE) + + assertThat(underTest.areNotificationsVisible).isTrue() } @Test - @DisableSceneContainer - fun areNotificationsVisible_withLargeClock_false() = + @EnableSceneContainer + fun areNotificationsVisible_dualShadeWideOnLockscreen_true() = kosmos.runTest { - val areNotificationsVisible by collectLastValue(underTest.areNotificationsVisible()) - fakeKeyguardClockRepository.setClockSize(ClockSize.LARGE) - assertThat(areNotificationsVisible).isFalse() + setupState( + shadeMode = ShadeMode.Dual, + clockSize = ClockSize.LARGE, + shadeLayoutWide = true, + ) + + assertThat(underTest.areNotificationsVisible).isTrue() } @Test - fun isShadeLayoutWide_withConfigTrue_true() = + @DisableSceneContainer + fun areNotificationsVisible_withSmallClock_true() = kosmos.runTest { - val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) - shadeRepository.setShadeLayoutWide(true) + setupState(shadeMode = ShadeMode.Single, clockSize = ClockSize.SMALL) - assertThat(isShadeLayoutWide).isTrue() + assertThat(underTest.areNotificationsVisible).isTrue() } @Test - fun isShadeLayoutWide_withConfigFalse_false() = + @DisableSceneContainer + fun areNotificationsVisible_withLargeClock_false() = kosmos.runTest { - val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) - shadeRepository.setShadeLayoutWide(false) + setupState(shadeMode = ShadeMode.Single, clockSize = ClockSize.LARGE) - assertThat(isShadeLayoutWide).isFalse() + assertThat(underTest.areNotificationsVisible).isFalse() } @Test fun unfoldTranslations() = kosmos.runTest { val maxTranslation = prepareConfiguration() - val translations by collectLastValue(underTest.unfoldTranslations) val unfoldProvider = fakeUnfoldTransitionProgressProvider unfoldProvider.onTransitionStarted() - assertThat(translations?.start).isEqualTo(0f) - assertThat(translations?.end).isEqualTo(-0f) + runCurrent() + assertThat(underTest.unfoldTranslations.start).isZero() + assertThat(underTest.unfoldTranslations.end).isZero() repeat(10) { repetition -> val transitionProgress = 0.1f * (repetition + 1) unfoldProvider.onTransitionProgress(transitionProgress) - assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation) - assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation) + runCurrent() + assertThat(underTest.unfoldTranslations.start) + .isEqualTo((1 - transitionProgress) * maxTranslation) + assertThat(underTest.unfoldTranslations.end) + .isEqualTo(-(1 - transitionProgress) * maxTranslation) } unfoldProvider.onTransitionFinishing() - assertThat(translations?.start).isEqualTo(0f) - assertThat(translations?.end).isEqualTo(-0f) + runCurrent() + assertThat(underTest.unfoldTranslations.start).isZero() + assertThat(underTest.unfoldTranslations.end).isZero() unfoldProvider.onTransitionFinished() - assertThat(translations?.start).isEqualTo(0f) - assertThat(translations?.end).isEqualTo(-0f) + runCurrent() + assertThat(underTest.unfoldTranslations.start).isZero() + assertThat(underTest.unfoldTranslations.end).isZero() } @Test fun isContentVisible_whenNotOccluded_visible() = kosmos.runTest { - val isContentVisible by collectLastValue(underTest.isContentVisible) - keyguardOcclusionRepository.setShowWhenLockedActivityInfo(false, null) runCurrent() - assertThat(isContentVisible).isTrue() + assertThat(underTest.isContentVisible).isTrue() } @Test fun isContentVisible_whenOccluded_notVisible() = kosmos.runTest { - val isContentVisible by collectLastValue(underTest.isContentVisible) - keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null) fakeKeyguardTransitionRepository.transitionTo( KeyguardState.LOCKSCREEN, KeyguardState.OCCLUDED, ) runCurrent() - assertThat(isContentVisible).isFalse() + assertThat(underTest.isContentVisible).isFalse() } @Test fun isContentVisible_whenOccluded_notVisible_evenIfShadeShown() = kosmos.runTest { - val isContentVisible by collectLastValue(underTest.isContentVisible) - keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null) fakeKeyguardTransitionRepository.transitionTo( KeyguardState.LOCKSCREEN, @@ -238,7 +254,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa sceneInteractor.snapToScene(Scenes.Shade, "") runCurrent() - assertThat(isContentVisible).isFalse() + assertThat(underTest.isContentVisible).isFalse() } @Test @@ -260,17 +276,16 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa @Test fun isContentVisible_whenOccluded_notVisibleInOccluded_visibleInAod() = kosmos.runTest { - val isContentVisible by collectLastValue(underTest.isContentVisible) keyguardOcclusionRepository.setShowWhenLockedActivityInfo(true, null) fakeKeyguardTransitionRepository.transitionTo( - KeyguardState.LOCKSCREEN, - KeyguardState.OCCLUDED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, ) runCurrent() sceneInteractor.snapToScene(Scenes.Shade, "") runCurrent() - assertThat(isContentVisible).isFalse() + assertThat(underTest.isContentVisible).isFalse() fakeKeyguardTransitionRepository.transitionTo(KeyguardState.OCCLUDED, KeyguardState.AOD) runCurrent() @@ -278,9 +293,32 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa sceneInteractor.snapToScene(Scenes.Lockscreen, "") runCurrent() - assertThat(isContentVisible).isTrue() + assertThat(underTest.isContentVisible).isTrue() } + private fun Kosmos.setupState( + shadeMode: ShadeMode, + clockSize: ClockSize, + shadeLayoutWide: Boolean? = null, + ) { + val isShadeLayoutWide by collectLastValue(kosmos.shadeRepository.isShadeLayoutWide) + val collectedClockSize by collectLastValue(kosmos.keyguardClockInteractor.clockSize) + val collectedShadeMode by collectLastValue(kosmos.shadeModeInteractor.shadeMode) + when (shadeMode) { + ShadeMode.Dual -> kosmos.enableDualShade(wideLayout = shadeLayoutWide) + ShadeMode.Single -> kosmos.enableSingleShade() + ShadeMode.Split -> kosmos.enableSplitShade() + } + fakeKeyguardClockRepository.setShouldForceSmallClock(clockSize == ClockSize.SMALL) + fakeKeyguardClockRepository.setClockSize(clockSize) + runCurrent() + if (shadeLayoutWide != null) { + assertThat(isShadeLayoutWide).isEqualTo(shadeLayoutWide) + } + assertThat(collectedShadeMode).isEqualTo(shadeMode) + assertThat(collectedClockSize).isEqualTo(clockSize) + } + private fun prepareConfiguration(): Int { val configuration = context.resources.configuration configuration.setLayoutDirection(Locale.US) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java index f5a71113235a..a7a0c24e2163 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java @@ -33,8 +33,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.WallpaperColors; -import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; @@ -68,7 +66,7 @@ import java.util.stream.Collectors; @SmallTest @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -public class MediaOutputAdapterTest extends SysuiTestCase { +public class MediaOutputAdapterLegacyTest extends SysuiTestCase { private static final String TEST_DEVICE_NAME_1 = "test_device_name_1"; private static final String TEST_DEVICE_NAME_2 = "test_device_name_2"; @@ -92,8 +90,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Captor private ArgumentCaptor<SeekBar.OnSeekBarChangeListener> mOnSeekBarChangeListenerCaptor; - private MediaOutputAdapter mMediaOutputAdapter; - private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder; + private MediaOutputAdapterLegacy mMediaOutputAdapter; + private MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy mViewHolder; private List<MediaDevice> mMediaDevices = new ArrayList<>(); private List<MediaItem> mMediaItems = new ArrayList<>(); MediaOutputSeekbar mSpyMediaOutputSeekbar; @@ -124,9 +122,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true)); mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false)); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar); } @@ -150,9 +148,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindPairNew_verifyView() { - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaItems.add(MediaItem.createPairNewDeviceMediaItem()); mMediaItems.add(MediaItem.createPairNewDeviceMediaItem()); @@ -175,9 +173,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { .map((item) -> item.getMediaDevice().get()) .collect(Collectors.toList())); when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -197,9 +195,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { .map((item) -> item.getMediaDevice().get()) .collect(Collectors.toList())); when(mMediaSwitchingController.getSessionName()).thenReturn(null); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -225,7 +223,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_bindNonRemoteConnectedDevice_verifyView() { when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -245,7 +243,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectableMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice2)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -264,7 +262,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectableMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice2)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -276,7 +274,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void onBindViewHolder_bindSingleConnectedRemoteDevice_verifyView() { when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -294,7 +292,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.hasOngoingSession()).thenReturn(true); when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -315,7 +313,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.isHostForOngoingSession()).thenReturn(true); when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(ImmutableList.of()); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -348,7 +346,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(false); when(mMediaSwitchingController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -498,7 +496,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT); when(mMediaDevice1.hasOngoingSession()).thenReturn(true); when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -522,7 +520,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_SUBSCRIPTION_REQUIRED); when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -545,7 +543,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice2.getSubtext()).thenReturn(SUBTEXT_AD_ROUTING_DISALLOWED); when(mMediaDevice2.getSubtextString()).thenReturn(deviceStatus); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_NONE); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -567,7 +565,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice1.getSubtextString()).thenReturn(TEST_CUSTOM_SUBTEXT); when(mMediaDevice1.hasOngoingSession()).thenReturn(true); when(mMediaDevice1.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -627,9 +625,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() { - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaItems.add(MediaItem.createPairNewDeviceMediaItem()); mMediaOutputAdapter.updateItems(); @@ -645,9 +643,9 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -663,11 +661,12 @@ public class MediaOutputAdapterTest extends SysuiTestCase { assertThat(mMediaDevice2.getState()).isEqualTo( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); - MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder); + MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy( + mViewHolder); mMediaOutputAdapter.getItemCount(); mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0); @@ -684,11 +683,12 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaDevice2.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED); when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP); - mMediaOutputAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); mMediaOutputAdapter.updateItems(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); - MediaOutputAdapter.MediaDeviceViewHolder spyMediaDeviceViewHolder = spy(mViewHolder); + MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy spyMediaDeviceViewHolder = spy( + mViewHolder); mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 1); spyMediaDeviceViewHolder.mContainerLayout.performClick(); @@ -715,7 +715,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { List<MediaDevice> selectableDevices = new ArrayList<>(); selectableDevices.add(mMediaDevice2); when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(selectableDevices); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -859,7 +859,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectedMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice1)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -899,16 +899,6 @@ public class MediaOutputAdapterTest extends SysuiTestCase { } @Test - public void updateColorScheme_triggerController() { - WallpaperColors wallpaperColors = WallpaperColors.fromBitmap( - Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)); - - mMediaOutputAdapter.updateColorScheme(wallpaperColors, true); - - verify(mMediaSwitchingController).setCurrentColorScheme(wallpaperColors, true); - } - - @Test public void updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() { mMediaOutputAdapter.updateItems(); List<MediaItem> updatedList = new ArrayList<>(); @@ -990,7 +980,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void multipleSelectedDevices_verifySessionView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -1011,7 +1001,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { public void multipleSelectedDevices_verifyCollapsedView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -1024,13 +1014,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void multipleSelectedDevices_expandIconClicked_verifyInitialView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mViewHolder.mEndTouchArea.performClick(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -1047,13 +1037,13 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() { initializeSession(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); mViewHolder.mEndTouchArea.performClick(); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1); @@ -1075,7 +1065,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(selectedDevices); when(mMediaSwitchingController.getDeselectableMediaDevice()).thenReturn(new ArrayList<>()); - mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter + mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter .onCreateViewHolder( new LinearLayout(mContext), MediaItem.MediaItemType.TYPE_DEVICE); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt index 0bba8bba2419..b23cd5e5547f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.notifications.ui.viewmodel +import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,6 +29,8 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData 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 @@ -39,10 +42,13 @@ import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -50,6 +56,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @@ -155,6 +162,36 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() { assertThat(underTest.showClock).isFalse() } + @Test + fun showMedia_activeMedia_true() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) + runCurrent() + + assertThat(underTest.showMedia).isTrue() + } + + @Test + fun showMedia_noActiveMedia_false() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = false)) + runCurrent() + + assertThat(underTest.showMedia).isFalse() + } + + @Test + fun showMedia_qsDisabled_false() = + testScope.runTest { + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(MediaData(active = true)) + kosmos.fakeDisableFlagsRepository.disableFlags.update { + it.copy(disable2 = DISABLE2_QUICK_SETTINGS) + } + runCurrent() + + assertThat(underTest.showMedia).isFalse() + } + private fun TestScope.lockDevice() { val currentScene by collectLastValue(sceneInteractor.currentScene) kosmos.powerInteractor.setAsleepForTest() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt index c775bfd75f6e..9e400a6c0a4c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractorTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.panels.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest @@ -34,6 +35,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class GridLayoutTypeInteractorTest : SysuiTestCase() { val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt index 2e7aeb433e04..9fe783b98046 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/QSColumnsInteractorTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.configurationRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.qs.panels.data.repository.QSColumnsRepository @@ -76,6 +77,7 @@ class QSColumnsInteractorTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun withDualShade_returnsCorrectValue() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt index fdbf42c9afd8..d5e502e99de5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelTest.kt @@ -21,6 +21,7 @@ import android.content.res.mainResources import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QQS @@ -36,6 +37,7 @@ import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -43,6 +45,7 @@ import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(ParameterizedAndroidJunit4::class) @SmallTest class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : SysuiTestCase() { @@ -63,6 +66,7 @@ class MediaInRowInLandscapeViewModelTest(private val testData: TestData) : Sysui } @Test + @EnableSceneContainer fun shouldMediaShowInRow() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt index 241cdbfbef83..4912c319bf2e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModelTest.kt @@ -21,6 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.testCase @@ -88,6 +89,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationNull_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -111,6 +113,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -133,6 +136,7 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test + @EnableSceneContainer fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 80c7026b0cea..23a0f6224fb7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -757,4 +757,46 @@ class SceneInteractorTest : SysuiTestCase() { verify(processor, never()).onSceneAboutToChange(any(), any()) } + + @Test + fun changeScene_sameScene_withFreeze() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + verify(processor, never()).onSceneAboutToChange(any(), any()) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0) + + underTest.changeScene( + toScene = Scenes.Lockscreen, + loggingReason = "test", + sceneState = KeyguardState.AOD, + forceSettleToTargetScene = true, + ) + + verify(processor).onSceneAboutToChange(Scenes.Lockscreen, KeyguardState.AOD) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(1) + } + + @Test + fun changeScene_sameScene_withoutFreeze() = + kosmos.runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + val processor = mock<SceneInteractor.OnSceneAboutToChangeListener>() + underTest.registerSceneStateProcessor(processor) + verify(processor, never()).onSceneAboutToChange(any(), any()) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0) + + underTest.changeScene( + toScene = Scenes.Lockscreen, + loggingReason = "test", + sceneState = KeyguardState.AOD, + forceSettleToTargetScene = false, + ) + + verify(processor, never()).onSceneAboutToChange(any(), any()) + assertThat(fakeSceneDataSource.freezeAndAnimateToCurrentStateCallCount).isEqualTo(0) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt index 668f568d7f46..d26e195d360a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos @@ -31,6 +32,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@EnableSceneContainer class ShadeModeInteractorImplTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -80,7 +82,7 @@ class ShadeModeInteractorImplTest : SysuiTestCase() { } @Test - fun isDualShade_settingEnabled_returnsTrue() = + fun isDualShade_settingEnabledSceneContainerEnabled_returnsTrue() = testScope.runTest { // TODO(b/391578667): Add a test case for user switching once the bug is fixed. val shadeMode by collectLastValue(underTest.shadeMode) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt index b8f66acf6413..dde867814159 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt @@ -48,6 +48,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -59,6 +60,7 @@ import org.mockito.kotlin.verify import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() { @@ -103,6 +105,7 @@ class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableSceneContainer fun hydrateShadeMode_dualShadeEnabled() = testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java index 93ba8e1317fa..064fd485dab4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.util.Log; +import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -52,6 +53,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @SmallTest +@FlakyTest(bugId = 395832204) @RunWith(AndroidJUnit4.class) public class PluginInstanceTest extends SysuiTestCase { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt index cabe4afdea60..5d1950670777 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -183,7 +184,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = null, promotedContent = PROMOTED_CONTENT, ), - 32L, + creationTime = 32L, ) val latest by collectLastValue(underTest.notificationChip) @@ -246,7 +247,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ) - val underTest = factory.create(startingNotif, 123L) + val underTest = factory.create(startingNotif, creationTime = 123L) val latest by collectLastValue(underTest.notificationChip) assertThat(latest).isNotNull() @@ -306,9 +307,10 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { } @Test - fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() = + fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrueWithTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = true + fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( @@ -325,12 +327,14 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(9000) } @Test - fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() = + fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalseWithNoTime() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(9000) val underTest = factory.create( @@ -347,11 +351,15 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { assertThat(latest).isNotNull() assertThat(latest!!.isAppVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() } @Test fun notificationChip_updatesWhenAppIsVisible() = kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(9000) + val underTest = factory.create( activeNotificationModel( @@ -365,32 +373,39 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) - activityManagerRepository.fake.setIsAppVisible(UID, false) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isNull() - activityManagerRepository.fake.setIsAppVisible(UID, true) + fakeSystemClock.setCurrentTimeMillis(11000) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) - activityManagerRepository.fake.setIsAppVisible(UID, false) + fakeSystemClock.setCurrentTimeMillis(13000) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = false) assertThat(latest!!.isAppVisible).isFalse() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(11000) + + fakeSystemClock.setCurrentTimeMillis(15000) + activityManagerRepository.fake.setIsAppVisible(UID, isAppVisible = true) + assertThat(latest!!.isAppVisible).isTrue() + assertThat(latest!!.lastAppVisibleTime).isEqualTo(15000) } - // Note: This test is theoretically impossible because the notification key should contain the - // UID, so if the UID changes then the key would also change and a new interactor would be - // created. But, test it just in case. @Test - fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() = + fun notificationChip_updatedUid_newUidIsIgnoredButOtherDataNotIgnored() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false - val hiddenUid = 100 - val shownUid = 101 + val originalUid = 100 + val newUid = 101 val underTest = factory.create( activeNotificationModel( key = "notif", - uid = hiddenUid, + uid = originalUid, statusBarChipIcon = mock(), promotedContent = PROMOTED_CONTENT, ), @@ -402,16 +417,34 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { // WHEN the notif gets a new UID that starts as visible activityManagerRepository.fake.startingIsAppVisibleValue = true + val newPromotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.shortCriticalText = "Arrived" + } + val newPromotedContent = newPromotedContentBuilder.build() underTest.setNotification( activeNotificationModel( key = "notif", - uid = shownUid, + uid = newUid, statusBarChipIcon = mock(), - promotedContent = PROMOTED_CONTENT, + promotedContent = newPromotedContent, ) ) - // THEN we re-fetch the app visibility state with the new UID + // THEN we do update other fields like promoted content + assertThat(latest!!.promotedContent).isEqualTo(newPromotedContent) + + // THEN we don't fetch the app visibility state for the new UID + assertThat(latest!!.isAppVisible).isFalse() + + // AND don't listen to updates for the new UID + activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = false) + activityManagerRepository.fake.setIsAppVisible(newUid, isAppVisible = true) + assertThat(latest!!.isAppVisible).isFalse() + + // AND we still use updates from the old UID + // TODO(b/364653005): This particular behavior isn't great, can we do better? + activityManagerRepository.fake.setIsAppVisible(originalUid, isAppVisible = true) assertThat(latest!!.isAppVisible).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt index d8e4cd927bec..7ed2bd38bcd2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt @@ -333,7 +333,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun shownNotificationChips_sortedBasedOnFirstAppearanceTime() = + fun shownNotificationChips_sortedByFirstAppearanceTime() = kosmos.runTest { val latest by collectLastValue(underTest.shownNotificationChips) @@ -349,8 +349,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), ) setNotifs(listOf(notif1)) - assertThat(latest).hasSize(1) - assertThat(latest!![0].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder() // WHEN we add notif2 at t=2000 fakeSystemClock.advanceTime(1000) @@ -362,26 +361,20 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { ) setNotifs(listOf(notif1, notif2)) - // THEN notif2 is ranked above notif1 because it appeared later - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + // THEN notif2 is ranked above notif1 because notif2 appeared later + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 and notif2 swap places setNotifs(listOf(notif2, notif1)) // THEN notif2 is still ranked above notif1 to preserve chip ordering - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 and notif2 swap places again setNotifs(listOf(notif1, notif2)) // THEN notif2 is still ranked above notif1 to preserve chip ordering - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 gets an update val notif1NewPromotedContent = @@ -400,9 +393,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { ) // THEN notif2 is still ranked above notif1 to preserve chip ordering - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif2") - assertThat(latest!![1].key).isEqualTo("notif1") + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() // WHEN notif1 disappears and then reappears fakeSystemClock.advanceTime(1000) @@ -413,9 +404,238 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { setNotifs(listOf(notif2, notif1)) // THEN notif1 is now ranked first - assertThat(latest).hasSize(2) - assertThat(latest!![0].key).isEqualTo("notif1") - assertThat(latest!![1].key).isEqualTo("notif2") + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_sortedByLastAppVisibleTime() = + kosmos.runTest { + val latest by collectLastValue(underTest.shownNotificationChips) + + val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) + val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) + + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(1000) + val notif1 = + activeNotificationModel( + key = notif1Info.key, + uid = notif1Info.uid, + statusBarChipIcon = notif1Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif1Info.key).build(), + ) + val notif2 = + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif2Info.key).build(), + ) + setNotifs(listOf(notif1, notif2)) + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN notif2's app becomes visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + + // THEN notif2 is no longer shown + assertThat(latest!!.map { it.key }).containsExactly("notif1").inOrder() + + // WHEN notif2's app is no longer visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN notif2 is ranked above notif1 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN the app associated with notif1 becomes visible then un-visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false) + + // THEN notif1 is now ranked above notif2 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_newNotificationTakesPriorityOverLastAppVisible() = + kosmos.runTest { + val latest by collectLastValue(underTest.shownNotificationChips) + + val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) + val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) + val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300) + + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(1000) + val notif1 = + activeNotificationModel( + key = notif1Info.key, + uid = notif1Info.uid, + statusBarChipIcon = notif1Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif1Info.key).build(), + ) + val notif2 = + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif2Info.key).build(), + ) + setNotifs(listOf(notif1, notif2)) + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN notif2's app becomes visible then not visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN notif2 is ranked above notif1 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN a new notif3 appears + fakeSystemClock.advanceTime(1000) + val notif3 = + activeNotificationModel( + key = notif3Info.key, + uid = notif3Info.uid, + statusBarChipIcon = notif3Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif3Info.key).build(), + ) + setNotifs(listOf(notif1, notif2, notif3)) + + // THEN notif3 is ranked above everything else + // AND notif2 is still before notif1 because it was more recently visible + assertThat(latest!!.map { it.key }) + .containsExactly("notif3", "notif2", "notif1") + .inOrder() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_fullSort() = + kosmos.runTest { + val latest by collectLastValue(underTest.shownNotificationChips) + + val notif1Info = NotifInfo("notif1", mock<StatusBarIconView>(), uid = 100) + val notif2Info = NotifInfo("notif2", mock<StatusBarIconView>(), uid = 200) + val notif3Info = NotifInfo("notif3", mock<StatusBarIconView>(), uid = 300) + + // First, add notif1 at t=1000 + activityManagerRepository.fake.startingIsAppVisibleValue = false + fakeSystemClock.setCurrentTimeMillis(1000) + val notif1 = + activeNotificationModel( + key = notif1Info.key, + uid = notif1Info.uid, + statusBarChipIcon = notif1Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif1Info.key).build(), + ) + setNotifs(listOf(notif1)) + + // WHEN we add notif2 at t=2000 + fakeSystemClock.advanceTime(1000) + val notif2 = + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif2Info.key).build(), + ) + setNotifs(listOf(notif1, notif2)) + + // THEN notif2 is ranked above notif1 because notif2 appeared later + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN notif2's app becomes visible then un-visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN notif2 is ranked above notif1 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif1").inOrder() + + // WHEN the app associated with notif1 becomes visible then un-visible + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif1Info.uid, isAppVisible = false) + + // THEN notif1 is ranked above notif2 because it was more recently visible + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN notif2 gets an update + val notif2NewPromotedContent = + PromotedNotificationContentModel.Builder("notif2").apply { + this.shortCriticalText = "Arrived" + } + setNotifs( + listOf( + notif1, + activeNotificationModel( + key = notif2Info.key, + uid = notif2Info.uid, + statusBarChipIcon = notif2Info.icon, + promotedContent = notif2NewPromotedContent.build(), + ), + ) + ) + + // THEN notif1 is still ranked above notif2 to preserve chip ordering + assertThat(latest!!.map { it.key }).containsExactly("notif1", "notif2").inOrder() + + // WHEN a new notification appears + fakeSystemClock.advanceTime(1000) + val notif3 = + activeNotificationModel( + key = notif3Info.key, + uid = notif3Info.uid, + statusBarChipIcon = notif3Info.icon, + promotedContent = + PromotedNotificationContentModel.Builder(notif3Info.key).build(), + ) + setNotifs(listOf(notif1, notif2, notif3)) + + // THEN it's ranked first because it's new + assertThat(latest!!.map { it.key }) + .containsExactly("notif3", "notif1", "notif2") + .inOrder() + + // WHEN notif2 becomes visible then un-visible again + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = true) + fakeSystemClock.advanceTime(1000) + activityManagerRepository.fake.setIsAppVisible(notif2Info.uid, isAppVisible = false) + + // THEN it moves to the front + assertThat(latest!!.map { it.key }) + .containsExactly("notif2", "notif3", "notif1") + .inOrder() + + // WHEN notif1 disappears and then reappears + fakeSystemClock.advanceTime(1000) + setNotifs(listOf(notif2, notif3)) + assertThat(latest!!.map { it.key }).containsExactly("notif2", "notif3").inOrder() + + fakeSystemClock.advanceTime(1000) + setNotifs(listOf(notif2, notif1, notif3)) + + // THEN notif1 is now ranked first + assertThat(latest!!.map { it.key }) + .containsExactly("notif1", "notif2", "notif3") + .inOrder() } @Test @@ -495,4 +715,6 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { .apply { notifs.forEach { addIndividualNotif(it) } } .build() } + + private data class NotifInfo(val key: String, val icon: StatusBarIconView, val uid: Int) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index aaa9b58a45df..7cf817a06225 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -49,8 +49,11 @@ import com.android.systemui.statusbar.notification.shared.ActiveNotificationMode import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.runner.RunWith @@ -286,13 +289,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasShortCriticalText_usesTextInsteadOfTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.shortCriticalText = "Arrived" this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -340,13 +345,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_basicTime_timeHiddenIfAutomaticallyPromoted() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.wasPromotedAutomatically = true this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -370,13 +377,15 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_basicTime_timeShownIfNotAutomaticallyPromoted() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.wasPromotedAutomatically = false this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 30.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -397,18 +406,117 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_basicTime_isShortTimeDelta() = + fun chips_basicTime_timeInFuture_isShortTimeDelta() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 3.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 13.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeLessThanOneMinInFuture_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 3.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime + 500, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeIsNow_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 62.seconds.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeInPast_isIconOnly() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 62.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime - 2.minutes.inWholeMilliseconds, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( listOf( activeNotificationModel( @@ -421,6 +529,45 @@ class NotifChipsViewModelTest : SysuiTestCase() { assertThat(latest).hasSize(1) assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java) + } + + // Not necessarily the behavior we *want* to have, but it's the currently implemented behavior. + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_timeIsInFuture_thenTimeAdvances_stillShortTimeDelta() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = currentTime + 3.minutes.inWholeMilliseconds, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = createStatusBarIconViewOrNull(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) + + fakeSystemClock.advanceTime(5.minutes.inWholeMilliseconds) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) .isInstanceOf(OngoingActivityChipModel.Active.ShortTimeDelta::class.java) } @@ -429,12 +576,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_countUpTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.CountUp, ) } @@ -457,12 +606,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_countDownTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.CountDown, ) } @@ -485,12 +636,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_noHeadsUp_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -517,12 +670,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpBySystem_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -556,12 +711,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -569,7 +726,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { PromotedNotificationContentModel.Builder("other notif").apply { this.time = PromotedNotificationContentModel.When( - time = 654321L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } @@ -610,12 +767,14 @@ class NotifChipsViewModelTest : SysuiTestCase() { fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() = kosmos.runTest { val latest by collectLastValue(underTest.chips) + val currentTime = 30.minutes.inWholeMilliseconds + fakeSystemClock.setCurrentTimeMillis(currentTime) val promotedContentBuilder = PromotedNotificationContentModel.Builder("notif").apply { this.time = PromotedNotificationContentModel.When( - time = 6543L, + time = currentTime + 10.minutes.inWholeMilliseconds, mode = PromotedNotificationContentModel.When.Mode.BasicTime, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingStateTest.kt new file mode 100644 index 000000000000..5dc59e893715 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingStateTest.kt @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2025 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.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.R.string.duration_hours_medium +import com.android.internal.R.string.duration_minutes_medium +import com.android.internal.R.string.now_string_shortest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class TimeRemainingStateTest : SysuiTestCase() { + + private var fakeTimeSource: MutableTimeSource = MutableTimeSource() + // We need a non-zero start time to advance to. This is needed to ensure `TimeRemainingState` is + // updated at least once. + private val startTime = 1.seconds.inWholeMilliseconds + + @Test + fun timeRemainingState_pastTime() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime - 62.seconds.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData).isNull() + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_lessThanOneMinute() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime + 59.seconds.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest) + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_lessThanOneMinuteInThePast() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime - 59.seconds.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest) + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_oneMinute() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime + 60.seconds.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(1) + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_lessThanOneHour() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime + 59.minutes.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(59) + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_oneHour() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime + 60.minutes.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(1) + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_betweenOneAndTwoHours() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime + 119.minutes.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + + assertThat(state.timeRemainingData).isNotNull() + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(1) + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_betweenFiveAndSixHours() = runTest { + val state = TimeRemainingState(fakeTimeSource, startTime + 320.minutes.inWholeMilliseconds) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(5) + job.cancelAndJoin() + } + + fun timeRemainingState_moreThan24Hours() = runTest { + val state = + TimeRemainingState(fakeTimeSource, startTime + (25 * 60.minutes.inWholeMilliseconds)) + val job = launch { state.run() } + + fakeTimeSource.time = startTime + advanceTimeBy(startTime) + assertThat(state.timeRemainingData).isNull() + + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_updateFromMinuteToNow() = runTest { + fakeTimeSource.time = startTime + val state = TimeRemainingState(fakeTimeSource, startTime + 119.seconds.inWholeMilliseconds) + val job = launch { state.run() } + + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(1) + + fakeTimeSource.time += 59.seconds.inWholeMilliseconds + advanceTimeBy(59.seconds.inWholeMilliseconds) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(1) + + fakeTimeSource.time += 1.seconds.inWholeMilliseconds + advanceTimeBy(1.seconds.inWholeMilliseconds) + assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest) + + job.cancelAndJoin() + } + + fun timeRemainingState_updateFromNowToEmpty() = runTest { + fakeTimeSource.time = startTime + val state = TimeRemainingState(fakeTimeSource, startTime) + val job = launch { state.run() } + + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(now_string_shortest) + + fakeTimeSource.time += 62.seconds.inWholeMilliseconds + advanceTimeBy(62.seconds.inWholeMilliseconds) + assertThat(state.timeRemainingData).isNull() + + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_updateFromHourToMinutes() = runTest { + fakeTimeSource.time = startTime + val state = TimeRemainingState(fakeTimeSource, startTime + 119.minutes.inWholeMilliseconds) + val job = launch { state.run() } + + advanceTimeBy(startTime) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(1) + + fakeTimeSource.time += 59.minutes.inWholeMilliseconds + advanceTimeBy(59.minutes.inWholeMilliseconds) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(1) + + fakeTimeSource.time += 1.seconds.inWholeMilliseconds + advanceTimeBy(1.seconds.inWholeMilliseconds) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_minutes_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(59) + + job.cancelAndJoin() + } + + @Test + fun timeRemainingState_showAfterLessThan24Hours() = runTest { + fakeTimeSource.time = startTime + val state = TimeRemainingState(fakeTimeSource, startTime + 25.hours.inWholeMilliseconds) + val job = launch { state.run() } + + advanceTimeBy(startTime) + assertThat(state.timeRemainingData).isNull() + + fakeTimeSource.time += 1.hours.inWholeMilliseconds + 1.seconds.inWholeMilliseconds + advanceTimeBy(1.hours.inWholeMilliseconds + 1.seconds.inWholeMilliseconds) + assertThat(state.timeRemainingData!!.first).isEqualTo(duration_hours_medium) + assertThat(state.timeRemainingData!!.second).isEqualTo(23) + + job.cancelAndJoin() + } + + /** A fake implementation of [TimeSource] that allows the caller to set the current time */ + private class MutableTimeSource(var time: Long = 0L) : TimeSource { + override fun getCurrentTime(): Long { + return time + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt index 1a5f57dd43f8..6409a20d5156 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt @@ -17,100 +17,119 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor import android.graphics.drawable.Drawable -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.parameterizeSceneContainerFlag +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +@RunWith(ParameterizedAndroidJunit4::class) @SmallTest -@RunWith(AndroidJUnit4::class) -class MediaControlChipInteractorTest : SysuiTestCase() { - +class MediaControlChipInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val underTest = kosmos.mediaControlChipInteractor + private val mediaFilterRepository = kosmos.mediaFilterRepository + private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipInteractor } + @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return parameterizeSceneContainerFlag() + } + } + + @Before + fun setUp() { + kosmos.underTest.initialize() + MockitoAnnotations.initMocks(this) + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } @Test - fun mediaControlModel_noActiveMedia_null() = + fun mediaControlChipModel_noActiveMedia_null() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) assertThat(model).isNull() } @Test - fun mediaControlModel_activeMedia_notNull() = + fun mediaControlChipModel_activeMedia_notNull() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val userMedia = MediaData(active = true) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() } @Test - fun mediaControlModel_mediaRemoved_null() = + fun mediaControlChipModel_mediaRemoved_null() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val userMedia = MediaData(active = true) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() - assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) - .isTrue() - mediaFilterRepository.addMediaDataLoadingState( - MediaDataLoadingModel.Removed(instanceId) - ) + removeMedia(userMedia) assertThat(model).isNull() } @Test - fun mediaControlModel_songNameChanged_emitsUpdatedModel() = + fun mediaControlChipModel_songNameChanged_emitsUpdatedModel() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val initialSongName = "Initial Song" val newSongName = "New Song" val userMedia = MediaData(active = true, song = initialSongName) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() assertThat(model?.songName).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat(model?.songName).isEqualTo(newSongName) } @Test - fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() = + fun mediaControlChipModel_playPauseActionChanges_emitsUpdatedModel() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val mockDrawable = mock<Drawable>() @@ -123,9 +142,7 @@ class MediaControlChipInteractorTest : SysuiTestCase() { ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) @@ -139,15 +156,15 @@ class MediaControlChipInteractorTest : SysuiTestCase() { ) val updatedMediaButton = MediaButton(playOrPause = newAction) val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat(model?.playOrPause).isEqualTo(newAction) } @Test - fun mediaControlModel_playPauseActionRemoved_playPauseNull() = + fun mediaControlChipModel_playPauseActionRemoved_playPauseNull() = kosmos.runTest { - val model by collectLastValue(underTest.mediaControlModel) + val model by collectLastValue(underTest.mediaControlChipModel) val mockDrawable = mock<Drawable>() @@ -160,16 +177,36 @@ class MediaControlChipInteractorTest : SysuiTestCase() { ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) - val instanceId = userMedia.instanceId - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) val updatedUserMedia = userMedia.copy(semanticActions = MediaButton()) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat(model?.playOrPause).isNull() } + + private fun updateMedia(mediaData: MediaData) { + if (SceneContainerFlag.isEnabled) { + val instanceId = mediaData.instanceId + mediaFilterRepository.addSelectedUserMediaEntry(mediaData) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + } else { + kosmos.underTest.updateMediaControlChipModelLegacy(mediaData) + } + } + + private fun removeMedia(mediaData: MediaData) { + if (SceneContainerFlag.isEnabled) { + val instanceId = mediaData.instanceId + mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, mediaData) + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Removed(instanceId) + ) + } else { + kosmos.underTest.updateMediaControlChipModelLegacy(null) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt index 8650e4b8cfce..d36dbbe8d36f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt @@ -16,26 +16,57 @@ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.parameterizeSceneContainerFlag +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import org.junit.Before import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class MediaControlChipViewModelTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val underTest = kosmos.mediaControlChipViewModel + private val mediaControlChipInteractor by lazy { kosmos.mediaControlChipInteractor } + private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipViewModel } + @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return parameterizeSceneContainerFlag() + } + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mediaControlChipInteractor.initialize() + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } @Test fun chip_noActiveMedia_IsHidden() = @@ -51,10 +82,7 @@ class MediaControlChipViewModelTest : SysuiTestCase() { val chip by collectLastValue(underTest.chip) val userMedia = MediaData(active = true, song = "test") - val instanceId = userMedia.instanceId - - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + updateMedia(userMedia) assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) } @@ -67,16 +95,25 @@ class MediaControlChipViewModelTest : SysuiTestCase() { val initialSongName = "Initial Song" val newSongName = "New Song" val userMedia = MediaData(active = true, song = initialSongName) - val instanceId = userMedia.instanceId - - mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) - + updateMedia(userMedia) + assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) - mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) + updateMedia(updatedUserMedia) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) } + + private fun updateMedia(mediaData: MediaData) { + if (SceneContainerFlag.isEnabled) { + val instanceId = mediaData.instanceId + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(mediaData) + kosmos.mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Loaded(instanceId) + ) + } else { + mediaControlChipInteractor.updateMediaControlChipModelLegacy(mediaData) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java index d66b010daefd..a58f7f72f08a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AssistantFeedbackControllerTest.java @@ -51,8 +51,10 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.systemui.SysuiTestCase; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.people.NotificationPersonExtractor; import com.android.systemui.util.DeviceConfigProxyFake; +import org.jetbrains.annotations.NotNull; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java index 77fd06757595..8520508c7611 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/DynamicChildBindControllerTest.java @@ -132,7 +132,7 @@ public class DynamicChildBindControllerTest extends SysuiTestCase { LayoutInflater inflater = LayoutInflater.from(mContext); inflater.setFactory2( new RowInflaterTask.RowAsyncLayoutInflater(entry, new FakeSystemClock(), mock( - RowInflaterTaskLogger.class))); + RowInflaterTaskLogger.class), mContext.getUser())); ExpandableNotificationRow row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row, null); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt new file mode 100644 index 000000000000..426af264da07 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection + +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +class BundleEntryTest : SysuiTestCase() { + private lateinit var underTest: BundleEntry + + @get:Rule + val setFlagsRule = SetFlagsRule() + + @Before + fun setUp() { + underTest = BundleEntry("key") + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getParent_adapter() { + assertThat(underTest.entryAdapter.parent).isEqualTo(GroupEntry.ROOT_ENTRY) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isTopLevelEntry_adapter() { + assertThat(underTest.entryAdapter.isTopLevelEntry).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getRow_adapter() { + assertThat(underTest.entryAdapter.row).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_adapter() { + assertThat(underTest.entryAdapter.groupRoot).isEqualTo(underTest.entryAdapter) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getKey_adapter() { + assertThat(underTest.entryAdapter.key).isEqualTo("key") + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java index 8e95ac599ce1..76e2d619a4df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/HighPriorityProviderTest.java @@ -30,6 +30,9 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -39,8 +42,10 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -57,6 +62,9 @@ public class HighPriorityProviderTest extends SysuiTestCase { @Mock private GroupMembershipManager mGroupMembershipManager; private HighPriorityProvider mHighPriorityProvider; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -210,6 +218,7 @@ public class HighPriorityProviderTest extends SysuiTestCase { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) public void testIsHighPriority_checkChildrenToCalculatePriority_legacy() { // GIVEN: a summary with low priority has a highPriorityChild and a lowPriorityChild final NotificationEntry summary = createNotifEntry(false); @@ -247,20 +256,18 @@ public class HighPriorityProviderTest extends SysuiTestCase { } @Test - public void testIsHighPriority_checkChildrenToCalculatePriority() { + public void testIsHighPriority_checkChildrenViewsToCalculatePriority() { // GIVEN: // parent with summary = lowPrioritySummary // NotificationEntry = lowPriorityChild // NotificationEntry = highPriorityChild + List<NotificationEntry> children = List.of(createNotifEntry(false), createNotifEntry(true)); final NotificationEntry lowPrioritySummary = createNotifEntry(false); final GroupEntry parentEntry = new GroupEntryBuilder() .setSummary(lowPrioritySummary) + .setChildren(children) .build(); - when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn( - new ArrayList<>( - List.of( - createNotifEntry(false), - createNotifEntry(true)))); + when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn(children); // THEN the GroupEntry parentEntry is high priority since it has a high priority child assertTrue(mHighPriorityProvider.isHighPriority(parentEntry)); @@ -272,10 +279,11 @@ public class HighPriorityProviderTest extends SysuiTestCase { // parent with summary = lowPrioritySummary // NotificationEntry = lowPriorityChild final NotificationEntry lowPrioritySummary = createNotifEntry(false); + final NotificationEntry lowPriorityChild = createNotifEntry(false); final GroupEntry parentEntry = new GroupEntryBuilder() .setSummary(lowPrioritySummary) + .setChildren(List.of(lowPriorityChild)) .build(); - final NotificationEntry lowPriorityChild = createNotifEntry(false); when(mGroupMembershipManager.getChildren(parentEntry)).thenReturn( new ArrayList<>(List.of(lowPriorityChild))); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt index e93c74252251..7fa157fa7cb3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt @@ -27,14 +27,29 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.notification.buildNotificationEntry +import com.android.systemui.statusbar.notification.buildOngoingCallEntry +import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry import com.android.systemui.statusbar.notification.collection.buildEntry import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner import com.android.systemui.statusbar.notification.collection.notifPipeline +import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.notification.promoted.domain.interactor.promotedNotificationsInteractor +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.testKosmos import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -59,7 +74,13 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { fun setup() { allowTestableLooperAsMainThread() - colorizedFgsCoordinator = ColorizedFgsCoordinator() + kosmos.statusBarNotificationChipsInteractor.start() + + colorizedFgsCoordinator = + ColorizedFgsCoordinator( + kosmos.applicationCoroutineScope, + kosmos.promotedNotificationsInteractor, + ) colorizedFgsCoordinator.attach(notifPipeline) sectioner = colorizedFgsCoordinator.sectioner } @@ -178,6 +199,37 @@ class ColorizedFgsCoordinatorTest : SysuiTestCase() { verify(notifPipeline, never()).addPromoter(any()) } + @Test + @EnableFlags( + PromotedNotificationUi.FLAG_NAME, + StatusBarNotifChips.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, + ) + fun comparatorPutsCallBeforeOther() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + kosmos.renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(kosmos.promotedNotificationsInteractor.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + + // VERIFY that the comparator puts the call before the ron + assertThat(sectioner.comparator!!.compare(callEntry, ronEntry)).isLessThan(0) + // VERIFY that the comparator puts the ron before the other + assertThat(sectioner.comparator!!.compare(ronEntry, otherEntry)).isLessThan(0) + } + private fun makeCallStyle(): Notification.CallStyle { val pendingIntent = PendingIntent.getBroadcast(mContext, 0, Intent("action"), PendingIntent.FLAG_IMMUTABLE) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index db5921d8bd36..3dd0982ba2ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -17,23 +17,29 @@ package com.android.systemui.statusbar.notification.collection.render import android.os.Build +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.log.assertLogsWtf +import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Assume import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.never @@ -44,6 +50,9 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidJUnit4::class) class GroupExpansionManagerTest : SysuiTestCase() { + @get:Rule + val setFlagsRule = SetFlagsRule() + private lateinit var underTest: GroupExpansionManagerImpl private val dumpManager: DumpManager = mock() @@ -52,8 +61,8 @@ class GroupExpansionManagerTest : SysuiTestCase() { private val pipeline: NotifPipeline = mock() private lateinit var beforeRenderListListener: OnBeforeRenderListListener - private val summary1 = notificationEntry("foo", 1) - private val summary2 = notificationEntry("bar", 1) + private val summary1 = notificationSummaryEntry("foo", 1) + private val summary2 = notificationSummaryEntry("bar", 1) private val entries = listOf<ListEntry>( GroupEntryBuilder() @@ -82,15 +91,25 @@ class GroupExpansionManagerTest : SysuiTestCase() { private fun notificationEntry(pkg: String, id: Int) = NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() } + private fun notificationSummaryEntry(pkg: String, id: Int) = + NotificationEntryBuilder().setPkg(pkg).setId(id).setParent(GroupEntry.ROOT_ENTRY).build() + .apply { row = mock() } + @Before fun setUp() { whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1) whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2) + whenever(groupMembershipManager.getGroupRoot(summary1.entryAdapter)) + .thenReturn(summary1.entryAdapter) + whenever(groupMembershipManager.getGroupRoot(summary2.entryAdapter)) + .thenReturn(summary2.entryAdapter) + underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager) } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun notifyOnlyOnChange() { var listenerCalledCount = 0 underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } @@ -108,6 +127,25 @@ class GroupExpansionManagerTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun notifyOnlyOnChange_withEntryAdapter() { + var listenerCalledCount = 0 + underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } + + underTest.setGroupExpanded(summary1.entryAdapter, false) + assertThat(listenerCalledCount).isEqualTo(0) + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(1) + underTest.setGroupExpanded(summary2.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(2) + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(listenerCalledCount).isEqualTo(2) + underTest.setGroupExpanded(summary2.entryAdapter, false) + assertThat(listenerCalledCount).isEqualTo(3) + } + + @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun expandUnattachedEntry() { // First, expand the entry when it is attached. underTest.setGroupExpanded(summary1, true) @@ -122,6 +160,22 @@ class GroupExpansionManagerTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun expandUnattachedEntryAdapter() { + // First, expand the entry when it is attached. + underTest.setGroupExpanded(summary1.entryAdapter, true) + assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue() + + // Un-attach it, and un-expand it. + NotificationEntryBuilder.setNewParent(summary1, null) + underTest.setGroupExpanded(summary1.entryAdapter, false) + + // Expanding again should throw. + assertLogsWtf { underTest.setGroupExpanded(summary1.entryAdapter, true) } + } + + @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun syncWithPipeline() { underTest.attach(pipeline) beforeRenderListListener = withArgCaptor { @@ -143,4 +197,28 @@ class GroupExpansionManagerTest : SysuiTestCase() { verify(listener).onGroupExpansionChange(summary1.row, false) verifyNoMoreInteractions(listener) } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun syncWithPipeline_withEntryAdapter() { + underTest.attach(pipeline) + beforeRenderListListener = withArgCaptor { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + val listener: OnGroupExpansionChangeListener = mock() + underTest.registerGroupExpansionChangeListener(listener) + + beforeRenderListListener.onBeforeRenderList(entries) + verify(listener, never()).onGroupExpansionChange(any(), any()) + + // Expand one of the groups. + underTest.setGroupExpanded(summary1.entryAdapter, true) + verify(listener).onGroupExpansionChange(summary1.row, true) + + // Empty the pipeline list and verify that the group is no longer expanded. + beforeRenderListListener.onBeforeRenderList(emptyList()) + verify(listener).onGroupExpansionChange(summary1.row, false) + verifyNoMoreInteractions(listener) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt index 2cbcc5a8d925..dcbf44e6e301 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt @@ -16,34 +16,46 @@ package com.android.systemui.statusbar.notification.collection.render +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.google.common.truth.Truth.assertThat +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class GroupMembershipManagerTest : SysuiTestCase() { + + @get:Rule + val setFlagsRule = SetFlagsRule() + private var underTest = GroupMembershipManagerImpl() @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_topLevel() { val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_noParent() { val noParentEntry = NotificationEntryBuilder().setParent(null).build() assertThat(underTest.isChildInGroup(noParentEntry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isChildInGroup_summary() { val groupKey = "group" val summary = @@ -57,12 +69,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.isGroupSummary(entry)).isFalse() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_summary() { val groupKey = "group" val summary = @@ -76,6 +90,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun isGroupSummary_child() { val groupKey = "group" val summary = @@ -90,12 +105,14 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() assertThat(underTest.getGroupSummary(entry)).isNull() } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_summary() { val groupKey = "group" val summary = @@ -109,6 +126,7 @@ class GroupMembershipManagerTest : SysuiTestCase() { } @Test + @DisableFlags(NotificationBundleUi.FLAG_NAME) fun getGroupSummary_child() { val groupKey = "group" val summary = @@ -121,4 +139,104 @@ class GroupMembershipManagerTest : SysuiTestCase() { assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary) } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_topLevel() { + val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.isChildInGroup(topLevelEntry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_noParent() { + val noParentEntry = NotificationEntryBuilder().setParent(null).build() + assertThat(underTest.isChildInGroup(noParentEntry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isChildEntryAdapterInGroup_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.isChildInGroup(summary.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_topLevelEntry() { + val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.isGroupRoot(summary.entryAdapter)).isTrue() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun isGroupRoot_child() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() + + assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_topLevelEntry() { + val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() + assertThat(underTest.getGroupRoot(entry.entryAdapter)).isNull() + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_summary() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() + + assertThat(underTest.getGroupRoot(summary.entryAdapter)).isEqualTo(summary.entryAdapter) + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + fun getGroupRoot_child() { + val groupKey = "group" + val summary = + NotificationEntryBuilder() + .setGroup(mContext, groupKey) + .setGroupSummary(mContext, true) + .build() + val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() + GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() + + assertThat(underTest.getGroupRoot(entry.entryAdapter)).isEqualTo(summary.entryAdapter) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt index 5ba972def76d..7cbc839c0ab5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt @@ -140,7 +140,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { @Test @EnableFlags(ModesEmptyShadeFix.FLAG_NAME) - @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @DisableFlags(Flags.FLAG_MODES_UI) fun text_changesWhenNotifsHiddenInShade() = testScope.runTest { val text by collectLastValue(underTest.text) @@ -163,7 +163,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI) fun text_changesWhenLocaleChanges() = testScope.runTest { val text by collectLastValue(underTest.text) @@ -186,7 +186,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI) fun text_reflectsModesHidingNotifications() = testScope.runTest { val text by collectLastValue(underTest.text) @@ -250,7 +250,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI) fun onClick_whenHistoryDisabled_leadsToSettingsPage() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) @@ -264,7 +264,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI) fun onClick_whenHistoryEnabled_leadsToHistoryPage() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) @@ -279,7 +279,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI) fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) @@ -305,7 +305,7 @@ class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI) fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() = testScope.runTest { val onClick by collectLastValue(underTest.onClick) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt index 339f8fac3820..e22acd53e584 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt @@ -106,11 +106,15 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { this.addOverride(R.integer.touch_acceptance_delay, TEST_TOUCH_ACCEPTANCE_TIME) this.addOverride( R.integer.heads_up_notification_minimum_time, - TEST_MINIMUM_DISPLAY_TIME, + TEST_MINIMUM_DISPLAY_TIME_DEFAULT, ) this.addOverride( R.integer.heads_up_notification_minimum_time_with_throttling, - TEST_MINIMUM_DISPLAY_TIME, + TEST_MINIMUM_DISPLAY_TIME_DEFAULT, + ) + this.addOverride( + R.integer.heads_up_notification_minimum_time_for_user_initiated, + TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED, ) this.addOverride(R.integer.heads_up_notification_decay, TEST_AUTO_DISMISS_TIME) this.addOverride( @@ -414,7 +418,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test - fun testRemoveNotification_beforeMinimumDisplayTime() { + fun testRemoveNotification_beforeMinimumDisplayTime_notUserInitiatedHun() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) useAccessibilityTimeout(false) @@ -429,18 +433,22 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(removedImmediately).isFalse() assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() - systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()) + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() } @Test - fun testRemoveNotification_afterMinimumDisplayTime() { + fun testRemoveNotification_afterMinimumDisplayTime_notUserInitiatedHun() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) useAccessibilityTimeout(false) underTest.showNotification(entry) - systemClock.advanceTime(((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()) + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_DEFAULT + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() @@ -455,6 +463,57 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun testRemoveNotification_beforeMinimumDisplayTime_forUserInitiatedHun() { + useAccessibilityTimeout(false) + + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + entry.row = testHelper.createRow() + underTest.showNotification(entry, isPinnedByUser = true) + + val removedImmediately = + underTest.removeNotification( + entry.key, + /* releaseImmediately = */ false, + "beforeMinimumDisplayTime", + ) + assertThat(removedImmediately).isFalse() + assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() + + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) + + assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun testRemoveNotification_afterMinimumDisplayTime_forUserInitiatedHun() { + useAccessibilityTimeout(false) + + val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) + entry.row = testHelper.createRow() + underTest.showNotification(entry, isPinnedByUser = true) + + systemClock.advanceTime( + ((TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED + TEST_AUTO_DISMISS_TIME) / 2).toLong() + ) + + assertThat(underTest.isHeadsUpEntry(entry.key)).isTrue() + + val removedImmediately = + underTest.removeNotification( + entry.key, + /* releaseImmediately = */ false, + "afterMinimumDisplayTime", + ) + + assertThat(removedImmediately).isTrue() + assertThat(underTest.isHeadsUpEntry(entry.key)).isFalse() + } + + @Test fun testRemoveNotification_releaseImmediately() { val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext) @@ -1047,16 +1106,21 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() { } companion object { - const val TEST_TOUCH_ACCEPTANCE_TIME = 200 - const val TEST_A11Y_AUTO_DISMISS_TIME = 1000 - const val TEST_EXTENSION_TIME = 500 + private const val TEST_TOUCH_ACCEPTANCE_TIME = 200 + private const val TEST_A11Y_AUTO_DISMISS_TIME = 1000 + private const val TEST_EXTENSION_TIME = 500 - const val TEST_MINIMUM_DISPLAY_TIME = 400 - const val TEST_AUTO_DISMISS_TIME = 600 - const val TEST_STICKY_AUTO_DISMISS_TIME = 800 + private const val TEST_MINIMUM_DISPLAY_TIME_DEFAULT = 400 + private const val TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED = 500 + private const val TEST_AUTO_DISMISS_TIME = 600 + private const val TEST_STICKY_AUTO_DISMISS_TIME = 800 init { - assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME) + assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT) + .isLessThan(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED) + assertThat(TEST_MINIMUM_DISPLAY_TIME_DEFAULT).isLessThan(TEST_AUTO_DISMISS_TIME) + assertThat(TEST_MINIMUM_DISPLAY_TIME_FOR_USER_INITIATED) + .isLessThan(TEST_AUTO_DISMISS_TIME) assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME) assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt index ee4d0990d38f..ee698ae20adb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt @@ -32,8 +32,12 @@ import com.android.systemui.statusbar.notification.data.repository.activeNotific import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor +import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style.Base import com.android.systemui.statusbar.notification.shared.byIsAmbient import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply +import com.android.systemui.statusbar.notification.shared.byIsPromoted import com.android.systemui.statusbar.notification.shared.byIsPulsing import com.android.systemui.statusbar.notification.shared.byIsRowDismissed import com.android.systemui.statusbar.notification.shared.byIsSilent @@ -58,12 +62,14 @@ class NotificationIconsInteractorTest : SysuiTestCase() { private val testScope = kosmos.testScope private val activeNotificationListRepository = kosmos.activeNotificationListRepository private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor + private val aodPromotedNotificationInteractor = kosmos.aodPromotedNotificationInteractor private val underTest = NotificationIconsInteractor( kosmos.activeNotificationsInteractor, kosmos.bubblesOptional, kosmos.headsUpNotificationIconInteractor, + kosmos.aodPromotedNotificationInteractor, kosmos.notificationsKeyguardViewStateRepository, ) @@ -141,6 +147,22 @@ class NotificationIconsInteractorTest : SysuiTestCase() { notificationsKeyguardInteractor.setNotificationsFullyHidden(true) assertThat(filteredSet).comparingElementsUsing(byIsPulsing).contains(true) } + + @Test + fun filteredEntrySet_showAodPromoted() { + testScope.runTest { + val filteredSet by collectLastValue(underTest.filteredNotifSet(showAodPromoted = true)) + assertThat(filteredSet).comparingElementsUsing(byIsPromoted).contains(true) + } + } + + @Test + fun filteredEntrySet_noAodPromoted() { + testScope.runTest { + val filteredSet by collectLastValue(underTest.filteredNotifSet(showAodPromoted = false)) + assertThat(filteredSet).comparingElementsUsing(byIsPromoted).doesNotContain(true) + } + } } @SmallTest @@ -326,4 +348,12 @@ private val testIcons = activeNotificationModel(key = "notif5", isLastMessageFromReply = true), activeNotificationModel(key = "notif6", isSuppressedFromStatusBar = true), activeNotificationModel(key = "notif7", isPulsing = true), + activeNotificationModel(key = "notif8", promotedContent = promotedContent("notif8", Base)), ) + +private fun promotedContent( + key: String, + style: PromotedNotificationContentModel.Style, +): PromotedNotificationContentModel { + return PromotedNotificationContentModel.Builder(key).apply { this.style = style }.build() +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt new file mode 100644 index 000000000000..75f5de0118d4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifierTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.people + +import android.app.Notification +import android.app.NotificationChannel +import android.content.pm.ShortcutInfo +import android.service.notification.NotificationListenerService.Ranking +import android.service.notification.StatusBarNotification +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.collection.GroupEntry +import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_FULL_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.mock + + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PeopleNotificationIdentifierTest : SysuiTestCase() { + + private lateinit var underTest: PeopleNotificationIdentifierImpl + + private val summary1 = notificationEntry("foo", 1, summary = true) + private val summary2 = notificationEntry("bar", 1, summary = true) + private val entries = + listOf<GroupEntry>( + GroupEntryBuilder() + .setSummary(summary1) + .setChildren( + listOf( + notificationEntry("foo", 2), + notificationEntry("foo", 3), + notificationEntry("foo", 4) + ) + ) + .build(), + GroupEntryBuilder() + .setSummary(summary2) + .setChildren( + listOf( + notificationEntry("bar", 2), + notificationEntry("bar", 3), + notificationEntry("bar", 4) + ) + ) + .build() + ) + + private fun notificationEntry( + pkg: String, + id: Int, + summary: Boolean = false + ): NotificationEntry { + val sbn = mock(StatusBarNotification::class.java) + Mockito.`when`(sbn.key).thenReturn("key") + Mockito.`when`(sbn.notification).thenReturn(mock(Notification::class.java)) + if (summary) + Mockito.`when`(sbn.notification.isGroupSummary).thenReturn(true) + return NotificationEntryBuilder().setPkg(pkg) + .setId(id) + .setSbn(sbn) + .build().apply { + row = mock(ExpandableNotificationRow::class.java) + } + } + + private fun personRanking(entry: NotificationEntry, personType: Int): Ranking { + val channel = NotificationChannel("person", "person", 4) + channel.setConversationId("parent", "person") + channel.setImportantConversation(true) + + val br = RankingBuilder(entry.ranking) + + when (personType) { + TYPE_NON_PERSON -> br.setIsConversation(false) + TYPE_PERSON -> { + br.setIsConversation(true) + br.setShortcutInfo(null) + } + + TYPE_IMPORTANT_PERSON -> { + br.setIsConversation(true) + br.setShortcutInfo(mock(ShortcutInfo::class.java)) + br.setChannel(channel) + } + + else -> { + br.setIsConversation(true) + br.setShortcutInfo(mock(ShortcutInfo::class.java)) + } + } + + return br.build() + } + + @Before + fun setUp() { + val personExtractor = object : NotificationPersonExtractor { + public override fun isPersonNotification(sbn: StatusBarNotification): Boolean { + return true + } + } + + underTest = PeopleNotificationIdentifierImpl( + personExtractor, + GroupMembershipManagerImpl() + ) + } + + private val Ranking.personTypeInfo + get() = when { + !isConversation -> TYPE_NON_PERSON + conversationShortcutInfo == null -> TYPE_PERSON + channel?.isImportantConversation == true -> TYPE_IMPORTANT_PERSON + else -> TYPE_FULL_PERSON + } + + @Test + fun getPeopleNotificationType_entryIsImportant() { + summary1.setRanking(personRanking(summary1, TYPE_IMPORTANT_PERSON)) + + assertThat(underTest.getPeopleNotificationType(summary1)).isEqualTo(TYPE_IMPORTANT_PERSON) + } + + @Test + fun getPeopleNotificationType_importantChild() { + entries.get(0).getChildren().get(0).setRanking( + personRanking(entries.get(0).getChildren().get(0), TYPE_IMPORTANT_PERSON) + ) + + assertThat(entries.get(0).summary?.let { underTest.getPeopleNotificationType(it) }) + .isEqualTo(TYPE_IMPORTANT_PERSON) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt new file mode 100644 index 000000000000..aa6e76d08c17 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorTest.kt @@ -0,0 +1,156 @@ +/* + * 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.notification.promoted.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.notification.buildNotificationEntry +import com.android.systemui.statusbar.notification.buildOngoingCallEntry +import com.android.systemui.statusbar.notification.buildPromotedOngoingEntry +import com.android.systemui.statusbar.notification.domain.interactor.renderNotificationListInteractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi +import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags( + PromotedNotificationUi.FLAG_NAME, + StatusBarNotifChips.FLAG_NAME, + StatusBarChipsModernization.FLAG_NAME, + StatusBarRootModernization.FLAG_NAME, +) +class PromotedNotificationsInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val Kosmos.underTest by Fixture { promotedNotificationsInteractor } + + @Before + fun setUp() { + kosmos.statusBarNotificationChipsInteractor.start() + } + + @Test + fun orderedChipNotificationKeys_containsNonPromotedCalls() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + } + + @Test + fun orderedChipNotificationKeys_containsPromotedCalls() = + kosmos.runTest { + // GIVEN a call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = true) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val orderedChipNotificationKeys by + collectLastValue(underTest.orderedChipNotificationKeys) + + // THEN the order of the notification keys should be the call then the RON + assertThat(orderedChipNotificationKeys) + .containsExactly("0|test_pkg|0|call|0", "0|test_pkg|0|ron|0") + } + + @Test + fun topPromotedNotificationContent_skipsNonPromotedCalls() = + kosmos.runTest { + // GIVEN a non-promoted call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = false) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN the ron is first because the call has no content + assertThat(topPromotedNotificationContent?.identity?.key) + .isEqualTo("0|test_pkg|0|ron|0") + } + + @Test + fun topPromotedNotificationContent_includesPromotedCalls() = + kosmos.runTest { + // GIVEN a promoted call and a promoted ongoing notification + val callEntry = buildOngoingCallEntry(promoted = true) + val ronEntry = buildPromotedOngoingEntry() + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList( + listOf(callEntry, ronEntry, otherEntry) + ) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN the call is the top notification + assertThat(topPromotedNotificationContent?.identity?.key) + .isEqualTo("0|test_pkg|0|call|0") + } + + @Test + fun topPromotedNotificationContent_nullWithNoPromotedNotifications() = + kosmos.runTest { + // GIVEN a a non-promoted call and no promoted ongoing entry + val callEntry = buildOngoingCallEntry(promoted = false) + val otherEntry = buildNotificationEntry(tag = "other") + + renderNotificationListInteractor.setRenderedList(listOf(callEntry, otherEntry)) + + val topPromotedNotificationContent by + collectLastValue(underTest.topPromotedNotificationContent) + + // THEN there is no top promoted notification + assertThat(topPromotedNotificationContent).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 7d406b4b397c..9f35d631bd45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -67,6 +67,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; @@ -248,14 +249,13 @@ public class NotificationContentInflaterTest extends SysuiTestCase { true /* isNewView */, (v, p, r) -> true, new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, - Exception e) { + public void handleInflationException(Exception e) { countDownLatch.countDown(); throw new RuntimeException("No Exception expected"); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { countDownLatch.countDown(); } }, mRow.getPrivateLayout(), null, null, new HashMap<>(), @@ -539,8 +539,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { inflater.setInflateSynchronously(true); InflationCallback callback = new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, - Exception e) { + public void handleInflationException(Exception e) { if (!expectingException) { exceptionHolder.setException(e); } @@ -548,7 +547,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { if (expectingException) { exceptionHolder.setException(new RuntimeException( "Inflation finished even though there should be an error")); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt index d43cc78e20dc..4c1f4f17e00c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.kt @@ -71,6 +71,10 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider +import com.android.systemui.statusbar.notification.row.icon.appIconProvider +import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.testKosmos @@ -203,6 +207,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( accessibilityManager, highPriorityProvider, iNotificationManager, + kosmos.appIconProvider, + kosmos.notificationIconStyleProvider, userManager, peopleSpaceWidgetManager, launcherApps, @@ -512,6 +518,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( .bindNotification( any<PackageManager>(), any<INotificationManager>(), + any<AppIconProvider>(), + any<NotificationIconStyleProvider>(), eq(onUserInteractionCallback), eq(channelEditorDialogController), eq(statusBarNotification.packageName), @@ -550,6 +558,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( .bindNotification( any<PackageManager>(), any<INotificationManager>(), + any<AppIconProvider>(), + any<NotificationIconStyleProvider>(), eq(onUserInteractionCallback), eq(channelEditorDialogController), eq(statusBarNotification.packageName), @@ -586,6 +596,8 @@ class NotificationGutsManagerTest(flags: FlagsParameterization) : SysuiTestCase( .bindNotification( any<PackageManager>(), any<INotificationManager>(), + any<AppIconProvider>(), + any<NotificationIconStyleProvider>(), eq(onUserInteractionCallback), eq(channelEditorDialogController), eq(statusBarNotification.packageName), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt index 2945fa98caad..96ae07035ed2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt @@ -64,6 +64,10 @@ import com.android.systemui.statusbar.RankingBuilder import com.android.systemui.statusbar.notification.AssistantFeedbackController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider +import com.android.systemui.statusbar.notification.row.icon.appIconProvider +import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider import com.android.telecom.telecomManager import com.google.common.truth.Truth.assertThat import java.util.concurrent.CountDownLatch @@ -862,6 +866,8 @@ class NotificationInfoTest : SysuiTestCase() { private fun bindNotification( pm: PackageManager = this.mockPackageManager, iNotificationManager: INotificationManager = this.mockINotificationManager, + appIconProvider: AppIconProvider = kosmos.appIconProvider, + iconStyleProvider: NotificationIconStyleProvider = kosmos.notificationIconStyleProvider, onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback, channelEditorDialogController: ChannelEditorDialogController = this.channelEditorDialogController, @@ -882,6 +888,8 @@ class NotificationInfoTest : SysuiTestCase() { underTest.bindNotification( pm, iNotificationManager, + appIconProvider, + iconStyleProvider, onUserInteractionCallback, channelEditorDialogController, pkg, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 82eca3735a71..ce3aee1d88d2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTIO import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.ConversationNotificationProcessor +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi @@ -223,12 +224,12 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { remoteViewClickHandler = { _, _, _ -> true }, callback = object : InflationCallback { - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { countDownLatch.countDown() throw RuntimeException("No Exception expected") } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { countDownLatch.countDown() } }, @@ -675,14 +676,14 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { inflater.setInflateSynchronously(true) val callback: InflationCallback = object : InflationCallback { - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { if (!expectingException) { exceptionHolder.exception = e } countDownLatch.countDown() } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { if (expectingException) { exceptionHolder.exception = RuntimeException( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index c39b252cd795..f2131da8f0bb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -615,7 +615,7 @@ public class NotificationTestHelper { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock, - mRowInflaterTaskLogger)); + mRowInflaterTaskLogger, UserHandle.of(entry.getSbn().getNormalizedUserId()))); mRow = (ExpandableNotificationRow) inflater.inflate( R.layout.status_bar_notification_row, null /* root */, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java index acdbd6237733..5638e0b434aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfoTest.java @@ -48,6 +48,8 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import org.junit.Before; import org.junit.Rule; @@ -57,8 +59,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.util.concurrent.CountDownLatch; - @SmallTest @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper @@ -82,6 +82,10 @@ public class PromotedNotificationInfoTest extends SysuiTestCase { @Mock private INotificationManager mMockINotificationManager; @Mock + private AppIconProvider mMockAppIconProvider; + @Mock + private NotificationIconStyleProvider mMockIconStyleProvider; + @Mock private PackageManager mMockPackageManager; @Mock private OnUserInteractionCallback mOnUserInteractionCallback; @@ -127,10 +131,11 @@ public class PromotedNotificationInfoTest extends SysuiTestCase { public void testBindNotification_setsOnClickListenerForFeedback() throws Exception { // Bind the notification to the Info object - final CountDownLatch latch = new CountDownLatch(1); mInfo.bindNotification( mMockPackageManager, mMockINotificationManager, + mMockAppIconProvider, + mMockIconStyleProvider, mOnUserInteractionCallback, mChannelEditorDialogController, TEST_PACKAGE_NAME, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt index 16c5c8a98253..531b30b9547a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt @@ -33,7 +33,12 @@ val byIsRowDismissed: Correspondence<ActiveNotificationModel, Boolean> = val byIsLastMessageFromReply: Correspondence<ActiveNotificationModel, Boolean> = Correspondence.transforming( { it?.isLastMessageFromReply }, - "has an isLastMessageFromReply value of" + "has an isLastMessageFromReply value of", ) val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> = Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of") +val byIsPromoted: Correspondence<ActiveNotificationModel, Boolean> = + Correspondence.transforming( + { it?.promotedContent != null }, + "has (or doesn't have) a promoted content model", + ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt index d570f18e35d8..6381b4e0fef7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -57,11 +57,12 @@ class NotificationShelfViewModelTest : SysuiTestCase() { statusBarStateController = mock() whenever(screenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true) } - private val underTest = kosmos.notificationShelfViewModel private val deviceEntryFaceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository private val keyguardRepository = kosmos.fakeKeyguardRepository - private val keyguardTransitionController = kosmos.lockscreenShadeTransitionController private val powerRepository = kosmos.fakePowerRepository + private val keyguardTransitionController by lazy { kosmos.lockscreenShadeTransitionController } + + private val underTest by lazy { kosmos.notificationShelfViewModel } @Test fun canModifyColorOfNotifications_whenKeyguardNotShowing() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt index 256da253588c..9c5d65ec12ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.statusbar.notification.stack +import android.os.UserHandle import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import android.testing.TestableLooper.RunWithLooper @@ -21,6 +22,7 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.shared.NotificationMinimalism import com.android.systemui.statusbar.notification.shelf.NotificationShelfIconContainer import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState @@ -978,7 +980,10 @@ open class NotificationShelfTest : SysuiTestCase() { ) { val sbnMock: StatusBarNotification = mock() val mockEntry = mock<NotificationEntry>().apply { whenever(this.sbn).thenReturn(sbnMock) } - val row = ExpandableNotificationRow(mContext, null, mockEntry) + val row = when (NotificationBundleUi.isEnabled) { + true -> ExpandableNotificationRow(mContext, null, UserHandle.CURRENT) + false -> ExpandableNotificationRow(mContext, null, mockEntry) + } whenever(ambientState.lastVisibleBackgroundChild).thenReturn(row) whenever(ambientState.isExpansionChanging).thenReturn(true) whenever(ambientState.expansionFraction).thenReturn(expansionFraction) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java index 8ec17dadcfe7..345ddae42798 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java @@ -46,9 +46,11 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationEntry.NotifEntryAdapter; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationContentView; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; @@ -133,9 +135,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); + final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); + when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter); when(enr.isChildInGroup()).thenReturn(true); when(enr.areChildrenExpanded()).thenReturn(false); @@ -144,7 +148,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + if (NotificationBundleUi.isEnabled()) { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter); + } else { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + } verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -169,7 +177,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -193,7 +202,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr).setUserExpanded(true); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); } @@ -207,9 +217,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { final ExpandableNotificationRow enr = mock(ExpandableNotificationRow.class); final NotificationContentView privateLayout = mock(NotificationContentView.class); final NotificationEntry enrEntry = mock(NotificationEntry.class); + final NotifEntryAdapter enrEntryAdapter = mock(NotifEntryAdapter.class); when(enr.getPrivateLayout()).thenReturn(privateLayout); when(enr.getEntry()).thenReturn(enrEntry); + when(enr.getEntryAdapter()).thenReturn(enrEntryAdapter); when(enr.isChildInGroup()).thenReturn(true); when(enr.areChildrenExpanded()).thenReturn(false); @@ -218,7 +230,11 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { enr, mock(View.class), false, onExpandedVisibleRunner); // THEN - verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + if (NotificationBundleUi.isEnabled()) { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntryAdapter); + } else { + verify(mGroupExpansionManager).toggleGroupExpansion(enrEntry); + } verify(enr, never()).setUserExpanded(anyBoolean()); verify(privateLayout, never()).setOnExpandedVisibleListener(any()); } @@ -244,6 +260,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { // THEN verify(mGroupExpansionManager, never()).toggleGroupExpansion(enrEntry); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); verify(enr, never()).setUserExpanded(anyBoolean()); verify(privateLayout, never()).setOnExpandedVisibleListener(any()); } @@ -272,7 +289,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr).toggleExpansionState(); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -299,7 +317,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr, never()).toggleExpansionState(); verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -326,7 +345,8 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr).toggleExpansionState(); verify(privateLayout).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } @Test @@ -353,6 +373,7 @@ public class StatusBarRemoteInputCallbackTest extends SysuiTestCase { verify(enr, never()).toggleExpansionState(); verify(privateLayout, never()).setOnExpandedVisibleListener(onExpandedVisibleRunner); verify(enr, never()).setUserExpanded(anyBoolean()); - verify(mGroupExpansionManager, never()).toggleGroupExpansion(any()); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotificationEntry.class)); + verify(mGroupExpansionManager, never()).toggleGroupExpansion(any(NotifEntryAdapter.class)); } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index ff1ffccfb2de..22e28d883c97 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -469,7 +469,7 @@ class ZenModeInteractorTest : SysuiTestCase() { } @Test - @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API) + @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI) fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() = kosmos.runTest { val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java new file mode 100644 index 000000000000..ecd04a47b8ae --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColorRule.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 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.theme; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + + +public class HardwareColorRule implements TestRule { + public String color = ""; + public String[] options = {}; + public boolean isTesting = false; + + @Override + public Statement apply(Statement base, Description description) { + HardwareColors hardwareColors = description.getAnnotation(HardwareColors.class); + if (hardwareColors != null) { + color = hardwareColors.color(); + options = hardwareColors.options(); + isTesting = true; + } + return base; + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java new file mode 100644 index 000000000000..0b8df2e2670e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/HardwareColors.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2025 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.theme; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface HardwareColors { + String color(); + String[] options(); +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 5cd0846ded7e..9a0b8125fb25 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -64,6 +64,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.monet.DynamicColors; @@ -77,6 +78,7 @@ import com.android.systemui.util.settings.SecureSettings; import com.google.common.util.concurrent.MoreExecutors; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -98,6 +100,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { private static final UserHandle MANAGED_USER_HANDLE = UserHandle.of(100); private static final UserHandle PRIVATE_USER_HANDLE = UserHandle.of(101); + @Rule + public HardwareColorRule rule = new HardwareColorRule(); + @Mock private JavaAdapter mJavaAdapter; @Mock @@ -148,13 +153,17 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<ContentObserver> mSettingsObserver; + @Mock + private SystemPropertiesHelper mSystemProperties; + @Before public void setup() { MockitoAnnotations.initMocks(this); + when(mFeatureFlags.isEnabled(Flags.MONET)).thenReturn(true); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); when(mUiModeManager.getContrast()).thenReturn(0.5f); - when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true); + when(mResources.getColor(eq(android.R.color.system_accent1_500), any())) .thenReturn(Color.RED); when(mResources.getColor(eq(android.R.color.system_accent2_500), any())) @@ -166,11 +175,20 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { when(mResources.getColor(eq(android.R.color.system_neutral2_500), any())) .thenReturn(Color.BLACK); + when(mResources.getStringArray(com.android.internal.R.array.theming_defaults)) + .thenReturn(rule.options); + + // should fallback to `*|TONAL_SPOT|home_wallpaper` + when(mSystemProperties.get("ro.boot.hardware.color")).thenReturn(rule.color); + // will try set hardware colors as boot ONLY if user is not set yet + when(mDeviceProvisionedController.isCurrentUserSetup()).thenReturn(!rule.isTesting); + mThemeOverlayController = new ThemeOverlayController(mContext, mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -214,11 +232,58 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { public void start_checksWallpaper() { ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); verify(mBgExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + } + + @Test + @HardwareColors(color = "BLK", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_checkHardwareColor() { + // getWallpaperColors should not be called + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager, never()).getWallpaperColors(anyInt()); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.MONOCHROMATIC); + assertThat(mThemeOverlayController.mCurrentColors.get(0).getMainColors().get( + 0).toArgb()).isEqualTo(Color.RED); + } + + @Test + @HardwareColors(color = "", options = { + "BLK|MONOCHROMATIC|#FF0000", + "*|VIBRANT|home_wallpaper" + }) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_wildcardColor() { + // getWallpaperColors will be called because we srt wildcard to `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); + registrationRunnable.getValue().run(); + verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.VIBRANT); + } + + @Test + @HardwareColors(color = "NONEXISTENT", options = {}) + @EnableFlags(com.android.systemui.Flags.FLAG_HARDWARE_COLOR_STYLES) + public void start_fallbackColor() { + // getWallpaperColors will be called because we default color source is `home_wallpaper` + ArgumentCaptor<Runnable> registrationRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mMainExecutor).execute(registrationRunnable.capture()); registrationRunnable.getValue().run(); verify(mWallpaperManager).getWallpaperColors(eq(WallpaperManager.FLAG_SYSTEM)); + + assertThat(mThemeOverlayController.mThemeStyle).isEqualTo(Style.TONAL_SPOT); } + @Test public void onWallpaperColorsChanged_setsTheme_whenForeground() { // Should ask for a new theme when wallpaper colors change @@ -287,9 +352,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"override.package.name\"," - + "\"android.theme.customization.color_source\":\"preset\"}"; + String jsonString = createJsonString(TestColorSource.preset, "override.package.name", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -313,11 +378,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -348,11 +409,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -381,11 +438,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -404,11 +457,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"lock_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.lock_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -455,8 +504,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { @Test public void onSettingChanged_invalidStyle() { when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true); - String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.theme_style\":\"some_invalid_name\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "A16B00", + "some_invalid_name"); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -473,11 +522,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -506,11 +551,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -537,11 +578,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -570,11 +607,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -599,7 +632,6 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } - @Test @EnableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI) public void onWallpaperColorsChanged_homeWallpaperWithSameColor_shouldKeepThemeAndReapply() { @@ -608,11 +640,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -642,11 +670,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -676,11 +700,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -711,11 +731,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(0xffa16b00), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -745,11 +761,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.color_source\":\"home_wallpaper\"," - + "\"android.theme.customization.system_palette\":\"A16B00\"," - + "\"android.theme.customization.accent_color\":\"A16B00\"," - + "\"android.theme.customization.color_index\":\"2\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper); when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) @@ -886,7 +898,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -926,7 +939,8 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier, mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController, mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle, - mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager) { + mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager, mActivityManager, + mSystemProperties) { @VisibleForTesting protected boolean isNightMode() { return false; @@ -992,7 +1006,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1018,7 +1032,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { clearInvocations(mThemeOverlayApplier); // Device went to sleep and second set of colors was applied. - mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), + mainColors = new WallpaperColors(Color.valueOf(Color.BLUE), Color.valueOf(Color.RED), null); mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM, USER_SYSTEM); @@ -1034,8 +1048,9 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), Color.valueOf(Color.BLUE), null); - String jsonString = - "{\"android.theme.customization.system_palette\":\"00FF00\"}"; + String jsonString = createJsonString(TestColorSource.home_wallpaper, "00FF00", + "TONAL_SPOT"); + when(mSecureSettings.getStringForUser( eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt())) .thenReturn(jsonString); @@ -1115,4 +1130,25 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { + DynamicColors.getCustomColorsMapped(false).size() * 2) ).setResourceValue(any(String.class), eq(TYPE_INT_COLOR_ARGB8), anyInt(), eq(null)); } + + private enum TestColorSource { + preset, + home_wallpaper, + lock_wallpaper + } + + private String createJsonString(TestColorSource colorSource, String seedColorHex, + String style) { + return "{\"android.theme.customization.color_source\":\"" + colorSource.toString() + "\"," + + "\"android.theme.customization.system_palette\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.accent_color\":\"" + seedColorHex + "\"," + + "\"android.theme.customization.color_index\":\"2\"," + + "\"android.theme.customization.theme_style\":\"" + style + "\"}"; + } + + private String createJsonString(TestColorSource colorSource) { + return createJsonString(colorSource, "A16B00", "TONAL_SPOT"); + } + + } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt index e484d8090c64..04ab98889755 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelTest.kt @@ -19,21 +19,17 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.bluetooth.BluetoothDevice import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.internal.logging.uiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.volume.data.repository.audioSharingRepository -import com.android.systemui.volume.domain.interactor.audioSharingInteractor import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -43,47 +39,30 @@ import org.mockito.kotlin.mock class AudioSharingStreamSliderViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private lateinit var stream: AudioSharingStreamSliderViewModel - - @Before - fun setUp() { - stream = audioSharingStreamSliderViewModel() - } - - private fun audioSharingStreamSliderViewModel(): AudioSharingStreamSliderViewModel { - return AudioSharingStreamSliderViewModel( - testScope.backgroundScope, - context, - kosmos.audioSharingInteractor, - kosmos.uiEventLogger, - kosmos.sliderHapticsViewModelFactory, - ) - } + private val underTest: AudioSharingStreamSliderViewModel = + with(kosmos) { audioSharingStreamSliderViewModelFactory.create(applicationCoroutineScope) } @Test fun slider_media_inAudioSharing() = - with(kosmos) { - testScope.runTest { - val audioSharingSlider by collectLastValue(stream.slider) + kosmos.runTest { + val audioSharingSlider by collectLastValue(underTest.slider) - val bluetoothDevice: BluetoothDevice = mock {} - val cachedDevice: CachedBluetoothDevice = mock { - on { groupId }.thenReturn(123) - on { device }.thenReturn(bluetoothDevice) - on { name }.thenReturn("my headset 2") - } - audioSharingRepository.setSecondaryDevice(cachedDevice) + val bluetoothDevice: BluetoothDevice = mock {} + val cachedDevice: CachedBluetoothDevice = mock { + on { groupId }.thenReturn(123) + on { device }.thenReturn(bluetoothDevice) + on { name }.thenReturn("my headset 2") + } + audioSharingRepository.setSecondaryDevice(cachedDevice) - audioSharingRepository.setInAudioSharing(true) - audioSharingRepository.setSecondaryGroupId(123) + audioSharingRepository.setInAudioSharing(true) + audioSharingRepository.setSecondaryGroupId(123) - runCurrent() + runCurrent() - assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2") - assertThat(audioSharingSlider!!.icon) - .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null)) - } + assertThat(audioSharingSlider!!.label).isEqualTo("my headset 2") + assertThat(audioSharingSlider!!.icon) + .isEqualTo(Icon.Resource(R.drawable.ic_volume_media_bt, null)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt index 7c166de81502..cc6a7b93eef3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt @@ -42,7 +42,6 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -53,7 +52,6 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class WallpaperRepositoryImplTest : SysuiTestCase() { - private var isWallpaperSupported = true private val kosmos = testKosmos().apply { @@ -293,12 +291,9 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { Intent(Intent.ACTION_WALLPAPER_CHANGED), ) assertThat(latest).isTrue() - assertThat(underTest.sendLockscreenLayoutJob).isNotNull() - assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true) } @Test - @Ignore("ag/31591766") @EnableFlags(SharedFlags.FLAG_EXTENDED_WALLPAPER_EFFECTS) fun shouldSendNotificationLayout_setNotExtendedEffectsWallpaper_cancelSendLayoutJob() = testScope.runTest { @@ -315,8 +310,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { Intent(Intent.ACTION_WALLPAPER_CHANGED), ) assertThat(latest).isTrue() - assertThat(underTest.sendLockscreenLayoutJob).isNotNull() - assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true) whenever(kosmos.wallpaperManager.getWallpaperInfoForUser(any())) .thenReturn(UNSUPPORTED_WP) @@ -327,7 +320,6 @@ class WallpaperRepositoryImplTest : SysuiTestCase() { runCurrent() assertThat(latest).isFalse() - assertThat(underTest.sendLockscreenLayoutJob?.isCancelled).isEqualTo(true) } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt index cd6e18a69c4d..31a611cc984b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractorTest.kt @@ -30,13 +30,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository -import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.testKosmos -import com.android.systemui.wallpapers.data.repository.fakeWallpaperFocalAreaRepository import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository -import com.android.systemui.wallpapers.data.repository.wallpaperRepository import com.android.systemui.wallpapers.ui.viewmodel.wallpaperFocalAreaViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -77,40 +72,26 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { .thenReturn(2f) underTest = WallpaperFocalAreaInteractor( - applicationScope = testScope.backgroundScope, context = kosmos.mockedContext, - wallpaperFocalAreaRepository = kosmos.fakeWallpaperFocalAreaRepository, + wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository, shadeRepository = kosmos.shadeRepository, - activeNotificationsInteractor = kosmos.activeNotificationsInteractor, - wallpaperRepository = kosmos.wallpaperRepository, ) } - private fun overrideMockedResources(overrideResources: OverrideResources) { - val displayMetrics = - DisplayMetrics().apply { - widthPixels = overrideResources.screenWidth - heightPixels = overrideResources.screenHeight - density = 2f - } - whenever(mockedResources.displayMetrics).thenReturn(displayMetrics) - whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape)) - .thenReturn(overrideResources.centerAlignFocalArea) - } - @Test fun focalAreaBounds_withoutNotifications_inHandheldDevices() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) - kosmos.activeNotificationListRepository.setActiveNotifs(0) + kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F) @@ -122,15 +103,15 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { fun focalAreaBounds_withNotifications_inHandheldDevices() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) - kosmos.activeNotificationListRepository.setActiveNotifs(1) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F) @@ -139,58 +120,38 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { } @Test - fun focalAreaBounds_withNotifications_inUnfoldLandscape() = + fun focalAreaBounds_inUnfoldLandscape() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 2000, screenHeight = 1600, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(true) - kosmos.activeNotificationListRepository.setActiveNotifs(1) - kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1400F) - kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F) - kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F) - - assertThat(bounds).isEqualTo(RectF(500f, 600F, 1000F, 1100F)) - } - - @Test - fun focalAreaBounds_withoutNotifications_inUnfoldLandscape() = - testScope.runTest { - overrideMockedResources( - OverrideResources( - screenWidth = 2000, - screenHeight = 1600, - centerAlignFocalArea = false, - ) - ) - val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) - kosmos.shadeRepository.setShadeLayoutWide(true) - kosmos.activeNotificationListRepository.setActiveNotifs(0) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1400F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F) - assertThat(bounds).isEqualTo(RectF(1000f, 600F, 1500F, 1100F)) + assertThat(bounds).isEqualTo(RectF(600f, 600F, 1400F, 1100F)) } @Test fun focalAreaBounds_withNotifications_inUnfoldPortrait() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) - kosmos.activeNotificationListRepository.setActiveNotifs(1) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F) @@ -202,15 +163,15 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { fun focalAreaBounds_withoutNotifications_inUnfoldPortrait() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1600, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(false) - kosmos.activeNotificationListRepository.setActiveNotifs(0) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(600F) @@ -219,18 +180,18 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { } @Test - fun focalAreaBounds_withNotifications_inTabletLandscape() = + fun focalAreaBounds_inTabletLandscape() = testScope.runTest { overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 3000, screenHeight = 2000, centerAlignFocalArea = true, - ) + ), ) val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) kosmos.shadeRepository.setShadeLayoutWide(true) - kosmos.activeNotificationListRepository.setActiveNotifs(1) kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F) kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(200F) kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(200F) @@ -239,35 +200,16 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { } @Test - fun focalAreaBounds_withoutNotifications_inTabletLandscape() = - testScope.runTest { - overrideMockedResources( - OverrideResources( - screenWidth = 3000, - screenHeight = 2000, - centerAlignFocalArea = true, - ) - ) - val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) - kosmos.shadeRepository.setShadeLayoutWide(true) - kosmos.activeNotificationListRepository.setActiveNotifs(0) - kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(1800F) - kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(400F) - kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(400F) - - assertThat(bounds).isEqualTo(RectF(1000f, 600F, 2000F, 1400F)) - } - - @Test fun onTap_inFocalBounds() = testScope.runTest { kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F)) overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds( RectF(250f, 700F, 750F, 1400F) @@ -287,11 +229,12 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { testScope.runTest { kosmos.wallpaperFocalAreaRepository.setTapPosition(PointF(0F, 0F)) overrideMockedResources( + mockedResources, OverrideResources( screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false, - ) + ), ) kosmos.wallpaperFocalAreaViewModel = mock() kosmos.wallpaperFocalAreaRepository.setWallpaperFocalAreaBounds( @@ -309,4 +252,21 @@ class WallpaperFocalAreaInteractorTest : SysuiTestCase() { val screenHeight: Int, val centerAlignFocalArea: Boolean, ) + + companion object { + fun overrideMockedResources( + mockedResources: Resources, + overrideResources: OverrideResources, + ) { + val displayMetrics = + DisplayMetrics().apply { + widthPixels = overrideResources.screenWidth + heightPixels = overrideResources.screenHeight + density = 2f + } + whenever(mockedResources.displayMetrics).thenReturn(displayMetrics) + whenever(mockedResources.getBoolean(R.bool.center_align_focal_area_shape)) + .thenReturn(overrideResources.centerAlignFocalArea) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt new file mode 100644 index 000000000000..3cd20721a15b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModelTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2025 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.wallpapers.ui.viewmodel + +import android.content.mockedContext +import android.content.res.Resources +import android.graphics.RectF +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.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +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.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs +import com.android.systemui.testKosmos +import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository +import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor +import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.Companion.overrideMockedResources +import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractorTest.OverrideResources +import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class WallpaperFocalAreaViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private lateinit var mockedResources: Resources + lateinit var underTest: WallpaperFocalAreaViewModel + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mockedResources = mock<Resources>() + overrideMockedResources( + mockedResources, + OverrideResources(screenWidth = 1000, screenHeight = 2000, centerAlignFocalArea = false), + ) + whenever(kosmos.mockedContext.resources).thenReturn(mockedResources) + whenever( + mockedResources.getFloat( + Resources.getSystem() + .getIdentifier( + /* name= */ "config_wallpaperMaxScale", + /* defType= */ "dimen", + /* defPackage= */ "android", + ) + ) + ) + .thenReturn(2f) + kosmos.wallpaperFocalAreaInteractor = + WallpaperFocalAreaInteractor( + context = kosmos.mockedContext, + wallpaperFocalAreaRepository = kosmos.wallpaperFocalAreaRepository, + shadeRepository = kosmos.shadeRepository, + ) + underTest = + WallpaperFocalAreaViewModel( + wallpaperFocalAreaInteractor = kosmos.wallpaperFocalAreaInteractor, + keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor, + ) + } + + @Test + fun focalAreaBoundsSent_whenFinishTransitioningToLockscreen() = + testScope.runTest { + overrideMockedResources( + mockedResources, + OverrideResources( + screenWidth = 1600, + screenHeight = 2000, + centerAlignFocalArea = false, + ), + ) + val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf( + TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN), + TransitionStep(transitionState = TransitionState.FINISHED, to = LOCKSCREEN), + ), + testScope, + ) + + setTestFocalAreaBounds() + + assertThat(bounds).isEqualTo(RectF(400F, 510F, 1200F, 700F)) + } + + @Test + fun focalAreaBoundsNotSent_whenNotFinishTransitioningToLockscreen() = + testScope.runTest { + overrideMockedResources( + mockedResources, + OverrideResources( + screenWidth = 1600, + screenHeight = 2000, + centerAlignFocalArea = false, + ), + ) + val bounds by collectLastValue(underTest.wallpaperFocalAreaBounds) + + kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( + listOf(TransitionStep(transitionState = TransitionState.STARTED, to = LOCKSCREEN)), + testScope, + ) + setTestFocalAreaBounds() + + assertThat(bounds).isEqualTo(null) + } + + private fun setTestFocalAreaBounds() { + kosmos.shadeRepository.setShadeLayoutWide(false) + kosmos.activeNotificationListRepository.setActiveNotifs(0) + kosmos.wallpaperFocalAreaRepository.setShortcutAbsoluteTop(400F) + kosmos.wallpaperFocalAreaRepository.setNotificationDefaultTop(20F) + kosmos.wallpaperFocalAreaRepository.setNotificationStackAbsoluteBottom(20F) + } +} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt index 9a837446a802..3ed321e48cd3 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt @@ -83,6 +83,13 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } } + fun updateAxes(lsFVar: String, aodFVar: String) { + i({ "updateAxes(LS = $str1, AOD = $str2)" }) { + str1 = lsFVar + str2 = aodFVar + } + } + fun addView(child: View) { d({ "addView($str1 @$int1)" }) { str1 = child::class.simpleName!! @@ -90,6 +97,14 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) : } } + fun animateDoze() { + d("animateDoze()") + } + + fun animateCharge() { + d("animateCharge()") + } + fun animateFidget(x: Float, y: Float) { d({ "animateFidget($str1, $str2)" }) { str1 = x.toString() diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml new file mode 100644 index 000000000000..a27e29f1beb6 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml @@ -0,0 +1,31 @@ +<!-- + ~ Copyright (C) 2025 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="40dp" + android:height="40dp" + android:viewportHeight="40" + android:viewportWidth="40"> + <group> + <clip-path android:pathData="M8,12h24.5v15.5h-24.5z" /> + <path + android:fillColor="#000000" + android:pathData="M30.75,12C29.79,12 29,12.79 29,13.75V25.75C29,26.71 29.79,27.5 30.75,27.5C31.71,27.5 32.5,26.71 32.5,25.75V13.75C32.5,12.79 31.71,12 30.75,12Z" /> + <path + android:fillColor="#000000" + android:pathData="M20.98,12.92C20.3,12.24 19.19,12.24 18.51,12.92C17.83,13.6 17.83,14.71 18.51,15.39L21.12,18H9.75C8.79,18 8,18.79 8,19.75C8,20.71 8.79,21.5 9.75,21.5H21.11L18.51,24.1C18.18,24.43 18,24.87 18,25.34C18,25.81 18.18,26.25 18.52,26.58C18.86,26.92 19.31,27.09 19.75,27.09C20.19,27.09 20.65,26.92 20.99,26.58L26.61,20.96C27.28,20.29 27.28,19.21 26.61,18.55L20.98,12.92Z" /> + </group> +</vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml new file mode 100644 index 000000000000..86f95bc97169 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_filled.xml @@ -0,0 +1,25 @@ +<!-- + ~ Copyright (C) 2025 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="40dp" + android:height="40dp" + android:viewportHeight="40" + android:viewportWidth="40"> + <path + android:fillColor="#000000" + android:pathData="M22.167,21.9L25.531,25.265C25.795,25.502 26.112,25.621 26.481,25.621C26.851,25.621 27.167,25.502 27.431,25.265C27.669,25.001 27.788,24.684 27.788,24.315C27.788,23.919 27.669,23.589 27.431,23.325L24.067,20L27.392,16.675C27.656,16.411 27.788,16.094 27.788,15.725C27.788,15.356 27.656,15.039 27.392,14.775C27.128,14.511 26.798,14.379 26.402,14.379C26.033,14.379 25.729,14.511 25.492,14.775L22.167,18.1L18.802,14.735C18.538,14.498 18.222,14.379 17.852,14.379C17.483,14.379 17.166,14.498 16.902,14.735C16.665,14.999 16.546,15.329 16.546,15.725C16.546,16.094 16.665,16.411 16.902,16.675L20.267,20L16.902,23.325C16.665,23.589 16.546,23.906 16.546,24.275C16.546,24.644 16.665,24.961 16.902,25.225C17.166,25.489 17.483,25.621 17.852,25.621C18.248,25.621 18.578,25.489 18.842,25.225L22.167,21.9ZM14.012,32.667C13.59,32.667 13.181,32.574 12.785,32.39C12.416,32.179 12.099,31.915 11.835,31.598L4.394,21.623C4.024,21.148 3.84,20.607 3.84,20C3.84,19.393 4.024,18.852 4.394,18.377L11.835,8.402C12.073,8.085 12.39,7.835 12.785,7.65C13.181,7.439 13.59,7.333 14.012,7.333H32.142C32.907,7.333 33.554,7.597 34.081,8.125C34.609,8.653 34.873,9.286 34.873,10.025V29.975C34.873,30.714 34.609,31.347 34.081,31.875C33.554,32.403 32.907,32.667 32.142,32.667H14.012Z" /> +</vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml new file mode 100644 index 000000000000..7f551f4d3c60 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete_outline.xml @@ -0,0 +1,31 @@ +<!-- + ~ Copyright (C) 2025 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="40dp" + android:height="40dp" + android:viewportHeight="40" + android:viewportWidth="40"> + <group> + <clip-path android:pathData="M5,7h29.89v25h-29.89z" /> + <path + android:fillColor="#000000" + android:pathData="M30.96,32H15.59C14.21,32 12.89,31.34 12.06,30.24L5.78,21.86C4.74,20.47 4.74,18.54 5.78,17.15L12.06,8.77C12.89,7.67 14.21,7 15.59,7H30.96C33.13,7 34.89,8.76 34.89,10.93V28.08C34.89,30.25 33.13,32.01 30.96,32.01V32ZM14.46,28.44C14.73,28.79 15.15,29 15.59,29H30.96C31.47,29 31.89,28.58 31.89,28.07V10.93C31.89,10.42 31.47,10 30.96,10H15.59C15.15,10 14.73,10.21 14.46,10.56L8.18,18.94C7.93,19.27 7.93,19.72 8.18,20.05L14.46,28.43V28.44Z" /> + <path + android:fillColor="#000000" + android:pathData="M22.46,21.27L25.36,24.17C25.6,24.43 25.89,24.56 26.25,24.56C26.61,24.56 26.9,24.43 27.14,24.17C27.4,23.93 27.53,23.64 27.53,23.28C27.53,22.92 27.4,22.63 27.14,22.39L24.24,19.49L27.14,16.59C27.38,16.35 27.49,16.06 27.49,15.7C27.49,15.34 27.37,15.05 27.14,14.81C26.91,14.57 26.61,14.46 26.25,14.46C25.89,14.46 25.59,14.58 25.33,14.81L22.46,17.71L19.56,14.81C19.32,14.55 19.03,14.42 18.67,14.42C18.31,14.42 18.02,14.55 17.78,14.81C17.52,15.05 17.39,15.34 17.39,15.7C17.39,16.06 17.52,16.35 17.78,16.59L20.68,19.49L17.78,22.39C17.52,22.63 17.39,22.92 17.39,23.28C17.39,23.64 17.52,23.93 17.78,24.17C18.02,24.41 18.31,24.52 18.67,24.52C19.03,24.52 19.32,24.4 19.56,24.17L22.46,21.27Z" /> + </group> +</vector> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml index 0b35559148af..87d06bfde743 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml @@ -21,7 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/keyguard_lock_padding" - android:importantForAccessibility="no" + android:accessibilityLiveRegion="polite" android:ellipsize="marquee" android:focusable="false" android:gravity="center" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml index f231df2f1a10..c7f320c69113 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_motion_layout.xml @@ -67,6 +67,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml index 04457229d573..9359838238af 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -32,6 +32,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml index b184344f2f24..6cbe96a8cb50 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_motion_layout.xml @@ -68,6 +68,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index 0e15ff66f3ee..cf388875a174 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -36,6 +36,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml index f6ac02aee657..33eab179c3f7 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_motion_layout.xml @@ -75,6 +75,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml index ba4da794d777..fd5eeb8b9408 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml @@ -33,6 +33,7 @@ <com.android.systemui.bouncer.ui.BouncerMessageView android:id="@+id/bouncer_message_view" android:screenReaderFocusable="true" + android:accessibilityLiveRegion="polite" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index bfb37a0d97a7..6d446453d9f7 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -31,6 +31,10 @@ <!-- height for the keyguard pin input field --> <dimen name="keyguard_pin_field_height">56dp</dimen> + <dimen name="keyguard_pattern_dot_size">16dp</dimen> + <dimen name="keyguard_pattern_activated_dot_size">24dp</dimen> + <dimen name="keyguard_pattern_stroke_width">32dp</dimen> + <!-- height for the keyguard password input field --> <dimen name="keyguard_password_field_height">56dp</dimen> diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 109e63c6167a..4472373f99a6 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -93,7 +93,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" - app:layout_constraintGuide_end="@dimen/qs_media_session_collapsed_guideline" /> + app:layout_constraintGuide_end="@dimen/qs_media_session_collapsed_legacy_guideline" /> <!-- App icon --> <com.android.internal.widget.CachingIconView diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid.xml b/packages/SystemUI/res/layout/notification_2025_hybrid.xml index 8fd10fb3ddb8..8c34cd4165e0 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid.xml @@ -29,7 +29,6 @@ android:layout_height="wrap_content" android:singleLine="true" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@*android:dimen/notification_2025_title_text_size" android:paddingEnd="4dp" /> <TextView diff --git a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml index 35f2ef901bdd..a338e4c70cfa 100644 --- a/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml +++ b/packages/SystemUI/res/layout/notification_2025_hybrid_conversation.xml @@ -54,7 +54,6 @@ android:singleLine="true" android:paddingEnd="4dp" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="@*android:dimen/notification_2025_title_text_size" /> <TextView diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 09aa2241e42b..8d10e393b5ca 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -119,7 +119,7 @@ <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" --> <string name="quick_settings_tiles_stock" translatable="false"> - internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes + internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects </string> <!-- The tiles to display in QuickSettings --> @@ -175,6 +175,9 @@ <!-- Minimum display time for a heads up notification if throttling is enabled, in milliseconds. --> <integer name="heads_up_notification_minimum_time_with_throttling">500</integer> + <!-- Minimum display time for a heads up notification that was shown from a user action (like tapping on a different part of the UI), in milliseconds. --> + <integer name="heads_up_notification_minimum_time_for_user_initiated">3000</integer> + <!-- Display time for a sticky heads up notification, in milliseconds. --> <integer name="sticky_heads_up_notification_time">60000</integer> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 3b1e609d86e2..8cd4c1bb3533 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -203,7 +203,7 @@ <!-- Size of the view displaying the mobile signal icon in the status bar. This value should match the core/status_bar_system_icon_size and change to sp unit --> <dimen name="status_bar_mobile_signal_size">15sp</dimen> - <dimen name="status_bar_mobile_signal_size_updated">14sp</dimen> + <dimen name="status_bar_mobile_signal_size_updated">12sp</dimen> <!-- Size of the view displaying the mobile signal icon in the status bar. This value should match the viewport height of mobile signal drawables such as ic_lte_mobiledata --> <dimen name="status_bar_mobile_type_size">16sp</dimen> @@ -1310,7 +1310,8 @@ <dimen name="qs_media_seekbar_progress_amplitude">1.5dp</dimen> <dimen name="qs_media_seekbar_progress_phase">8dp</dimen> <dimen name="qs_media_seekbar_progress_stroke_width">2dp</dimen> - <dimen name="qs_media_session_collapsed_guideline">144dp</dimen> + <dimen name="qs_media_session_collapsed_legacy_guideline">144dp</dimen> + <dimen name="qs_media_session_collapsed_guideline">168dp</dimen> <!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> <dimen name="qs_media_rec_default_width">380dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e077b41a6f59..c0eea15b043b 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2341,7 +2341,9 @@ <!-- User visible title for the keyboard shortcut that enters split screen with current app on the left [CHAR LIMIT=70] --> <string name="system_multitasking_lhs">Use split screen with app on the left</string> <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] --> - <string name="system_multitasking_full_screen">Switch to full screen</string> + <string name="system_multitasking_full_screen">Use full screen</string> + <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] --> + <string name="system_multitasking_desktop_view">Use desktop view</string> <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] --> <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string> <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] --> diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml index d885e00fbe82..faf06f3d39f0 100644 --- a/packages/SystemUI/res/values/tiles_states_strings.xml +++ b/packages/SystemUI/res/values/tiles_states_strings.xml @@ -358,4 +358,14 @@ <item>Off</item> <item>On</item> </string-array> + + <!-- State names for desktop effects tile: unavailable, off, on. + This subtitle is shown when the tile is in that particular state but does not set its own + subtitle, so some of these may never appear on screen. They should still be translated as + if they could appear. [CHAR LIMIT=32] --> + <string-array name="tile_states_desktopeffects"> + <item>Unavailable</item> + <item>Off</item> + <item>On</item> + </string-array> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 66c54a389c8e..b5efd04eeba9 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -64,6 +64,13 @@ app:layout_constraintBottom_toBottomOf="@+id/album_art" /> <Constraint + android:id="@+id/action_button_guideline" + android:layout_width="0dp" + android:layout_height="0dp" + android:orientation="vertical" + app:layout_constraintGuide_end="@dimen/qs_media_session_collapsed_legacy_guideline" /> + + <Constraint android:id="@+id/header_title" android:layout_width="0dp" android:layout_height="wrap_content" diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java index f528ec8af134..860a496ef18b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageAreaController.java @@ -20,22 +20,16 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.hardware.biometrics.BiometricSourceType; import android.os.SystemClock; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; import android.util.Log; import android.util.Pair; import android.view.View; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.util.ViewController; -import java.lang.ref.WeakReference; - import javax.inject.Inject; /** @@ -54,39 +48,8 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> private Pair<BiometricSourceType, Long> mMessageBiometricSource = null; private static final Long SKIP_SHOWING_FACE_MESSAGE_AFTER_FP_MESSAGE_MS = 3500L; - /** - * Delay before speaking an accessibility announcement. Used to prevent - * lift-to-type from interrupting itself. - */ - private static final long ANNOUNCEMENT_DELAY = 250; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final ConfigurationController mConfigurationController; - private final AnnounceRunnable mAnnounceRunnable; - private final TextWatcher mTextWatcher = new TextWatcher() { - @Override - public void afterTextChanged(Editable editable) { - CharSequence msg = editable; - if (!TextUtils.isEmpty(msg)) { - mView.removeCallbacks(mAnnounceRunnable); - mAnnounceRunnable.setTextToAnnounce(msg); - mView.postDelayed(() -> { - if (msg == mView.getText()) { - mAnnounceRunnable.run(); - } - }, ANNOUNCEMENT_DELAY); - } - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - /* no-op */ - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - /* no-op */ - } - }; private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() { public void onFinishedGoingToSleep(int why) { @@ -122,7 +85,6 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mKeyguardUpdateMonitor = keyguardUpdateMonitor; mConfigurationController = configurationController; - mAnnounceRunnable = new AnnounceRunnable(mView); } @Override @@ -131,14 +93,12 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> mKeyguardUpdateMonitor.registerCallback(mInfoCallback); mView.setSelected(mKeyguardUpdateMonitor.isDeviceInteractive()); mView.onThemeChanged(); - mView.addTextChangedListener(mTextWatcher); } @Override protected void onViewDetached() { mConfigurationController.removeCallback(mConfigurationListener); mKeyguardUpdateMonitor.removeCallback(mInfoCallback); - mView.removeTextChangedListener(mTextWatcher); } /** @@ -232,30 +192,4 @@ public class KeyguardMessageAreaController<T extends KeyguardMessageArea> view, mKeyguardUpdateMonitor, mConfigurationController); } } - - /** - * Runnable used to delay accessibility announcements. - */ - @VisibleForTesting - public static class AnnounceRunnable implements Runnable { - private final WeakReference<View> mHost; - private CharSequence mTextToAnnounce; - - AnnounceRunnable(View host) { - mHost = new WeakReference<>(host); - } - - /** Sets the text to announce. */ - public void setTextToAnnounce(CharSequence textToAnnounce) { - mTextToAnnounce = textToAnnounce; - } - - @Override - public void run() { - final View host = mHost.get(); - if (host != null && host.isVisibleToUser()) { - host.announceForAccessibility(mTextToAnnounce); - } - } - } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java index 5d63c2a92ba8..4a4cb7a232c5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java @@ -43,6 +43,8 @@ import com.android.internal.widget.LockPatternView; import com.android.settingslib.animation.AppearAnimationCreator; import com.android.settingslib.animation.AppearAnimationUtils; import com.android.settingslib.animation.DisappearAnimationUtils; +import com.android.systemui.Flags; +import com.android.systemui.bouncer.shared.constants.PatternBouncerConstants.ColorId; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt; @@ -227,6 +229,18 @@ public class KeyguardPatternView extends KeyguardInputView super.onFinishInflate(); mLockPatternView = findViewById(R.id.lockPatternView); + if (Flags.bouncerUiRevamp2()) { + mLockPatternView.setDotColors(mContext.getColor(ColorId.dotColor), mContext.getColor( + ColorId.activatedDotColor)); + mLockPatternView.setColors(mContext.getColor(ColorId.pathColor), 0, 0); + mLockPatternView.setDotSizes( + getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_dot_size), + getResources().getDimensionPixelSize( + R.dimen.keyguard_pattern_activated_dot_size)); + mLockPatternView.setPathWidth( + getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_stroke_width)); + mLockPatternView.setKeepDotActivated(true); + } mEcaView = findViewById(R.id.keyguard_selector_fade_container); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index fbe9edfd6680..04d4c2a3cdf9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -41,6 +41,7 @@ import androidx.annotation.CallSuper; import com.android.app.animation.Interpolators; import com.android.internal.widget.LockscreenCredential; +import com.android.systemui.Flags; import com.android.systemui.res.R; import java.util.ArrayList; @@ -178,7 +179,15 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView mOkButton = findViewById(R.id.key_enter); + if (Flags.bouncerUiRevamp2()) { + mOkButton.setImageResource(R.drawable.pin_bouncer_confirm); + } mDeleteButton = findViewById(R.id.delete_button); + if (Flags.bouncerUiRevamp2()) { + mDeleteButton.setDrawableForTransparentMode(R.drawable.pin_bouncer_delete_filled); + mDeleteButton.setDefaultDrawable(R.drawable.pin_bouncer_delete_outline); + mDeleteButton.setImageResource(R.drawable.pin_bouncer_delete_outline); + } mDeleteButton.setVisibility(View.VISIBLE); mButtons[0] = findViewById(R.id.key0); diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java index 2f74158107f2..69e4fd7c3d53 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java @@ -15,6 +15,7 @@ */ package com.android.keyguard; +import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ValueAnimator; @@ -33,6 +34,9 @@ import com.android.systemui.Flags; import com.android.systemui.bouncer.shared.constants.PinBouncerConstants.Animation; import com.android.systemui.bouncer.shared.constants.PinBouncerConstants.Color; +import java.util.ArrayList; +import java.util.List; + /** * Provides background color and radius animations for key pad buttons. */ @@ -141,6 +145,7 @@ class NumPadAnimator { mExpandAnimator.addUpdateListener( anim -> mBackground.setCornerRadius((float) anim.getAnimatedValue())); + List<Animator> expandAnimators = new ArrayList<>(); ValueAnimator expandBackgroundColorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), mNormalBackgroundColor, mPressedBackgroundColor); expandBackgroundColorAnimator.setDuration(Animation.expansionColorDuration); @@ -162,10 +167,27 @@ class NumPadAnimator { } }); + expandAnimators.add(mExpandAnimator); + expandAnimators.add(expandBackgroundColorAnimator); + expandAnimators.add(expandTextColorAnimator); + + if (Flags.bouncerUiRevamp2()) { + ValueAnimator expandTextScaleAnimator = ValueAnimator.ofFloat( + Animation.normalTextScaleX, Animation.pressedTextScaleX); + expandTextScaleAnimator.setInterpolator(Animation.expansionInterpolator); + expandTextScaleAnimator.setDuration(Animation.expansionDuration); + expandTextScaleAnimator.addUpdateListener(valueAnimator -> { + if (mDigitTextView != null) { + mDigitTextView.setTextScaleX((Float) valueAnimator.getAnimatedValue()); + } + }); + expandAnimators.add(expandTextScaleAnimator); + } + mExpandAnimatorSet = new AnimatorSet(); - mExpandAnimatorSet.playTogether(mExpandAnimator, - expandBackgroundColorAnimator, expandTextColorAnimator); + mExpandAnimatorSet.playTogether(expandAnimators); + List<Animator> contractAnimators = new ArrayList<>(); mContractAnimator = ValueAnimator.ofFloat(1f, 0f); mContractAnimator.setStartDelay(Animation.contractionStartDelay); mContractAnimator.setDuration(Animation.contractionDuration); @@ -195,9 +217,24 @@ class NumPadAnimator { } }); + contractAnimators.add(mContractAnimator); + contractAnimators.add(contractBackgroundColorAnimator); + contractAnimators.add(contractTextColorAnimator); + + if (Flags.bouncerUiRevamp2()) { + ValueAnimator contractTextScaleAnimator = ValueAnimator.ofFloat( + Animation.pressedTextScaleX, Animation.normalTextScaleX); + contractTextScaleAnimator.setInterpolator(Animation.contractionRadiusInterpolator); + contractTextScaleAnimator.setDuration(Animation.contractionDuration); + contractTextScaleAnimator.addUpdateListener(valueAnimator -> { + if (mDigitTextView != null) { + mDigitTextView.setTextScaleX((Float) valueAnimator.getAnimatedValue()); + } + }); + contractAnimators.add(contractTextScaleAnimator); + } mContractAnimatorSet = new AnimatorSet(); - mContractAnimatorSet.playTogether(mContractAnimator, - contractBackgroundColorAnimator, contractTextColorAnimator); + mContractAnimatorSet.playTogether(contractAnimators); } } diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 0ff93236a856..584ebb50520a 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -25,6 +25,7 @@ import android.util.AttributeSet; import android.view.MotionEvent; import android.view.accessibility.AccessibilityNodeInfo; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import com.android.systemui.Flags; @@ -42,6 +43,12 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni private int mStyleAttr; private boolean mIsTransparentMode; + @DrawableRes + private int mDrawableForTransparentMode = 0; + + @DrawableRes + private int mDefaultDrawable = 0; + public NumPadButton(Context context, AttributeSet attrs) { super(context, attrs); mStyleAttr = attrs.getStyleAttribute(); @@ -123,8 +130,14 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni mIsTransparentMode = isTransparentMode; if (isTransparentMode) { + if (mDrawableForTransparentMode != 0) { + setImageResource(mDrawableForTransparentMode); + } setBackgroundColor(getResources().getColor(android.R.color.transparent)); } else { + if (mDefaultDrawable != 0) { + setImageResource(mDefaultDrawable); + } Drawable bgDrawable = getContext().getDrawable(R.drawable.num_pad_key_background); if (Flags.bouncerUiRevamp2() && bgDrawable != null) { bgDrawable.setTint(Color.actionBg); @@ -154,4 +167,19 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni super.onInitializeAccessibilityNodeInfo(info); info.setTextEntryKey(true); } + + /** + * Drawable to use when transparent mode is enabled + */ + public void setDrawableForTransparentMode(@DrawableRes int drawableResId) { + mDrawableForTransparentMode = drawableResId; + } + + /** + * Drawable to use when transparent mode is not enabled. + */ + public void setDefaultDrawable(@DrawableRes int drawableResId) { + mDefaultDrawable = drawableResId; + setImageResource(mDefaultDrawable); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java index b152ff348e22..56aadc342424 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadKey.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadKey.java @@ -148,7 +148,7 @@ public class NumPadKey extends ViewGroup implements NumPadAnimationListener { if (bouncerUiRevamp2()) { mDigitText.setTypeface( - Typeface.create(FontStyles.GSF_LABEL_LARGE_EMPHASIZED, Typeface.NORMAL)); + Typeface.create(FontStyles.GSF_LABEL_SMALL_EMPHASIZED, Typeface.NORMAL)); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index c14d28d1c08d..3b6f8f87a1a8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -174,13 +174,14 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // Notify the service to update the magnifier scale only when the progress changed is - // triggered by user interaction on seekbar - if (fromUser) { - final float scale = transformProgressToScale(progress); - // We don't need to update the persisted scale when the seekbar progress is - // changing. The update should be triggered when the changing is ended. - mCallback.onMagnifierScale(scale, /* updatePersistence= */ false); + // triggered by user interaction on seekbar. + if (!fromUser) { + return; } + final float scale = transformProgressToScale(progress); + // We don't need to update the persisted scale when the seekbar progress is + // changing. The update should be triggered when the changing is ended. + mCallback.onMagnifierScale(scale, /* updatePersistence= */ false); } @Override @@ -195,7 +196,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Override public void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control) { - // Update the Settings persisted scale only when user interaction with seekbar ends + // Update the Settings persisted scale only when user interaction with seekbar ends. final int progress = seekBar.getProgress(); final float scale = transformProgressToScale(progress); mCallback.onMagnifierScale(scale, /* updatePersistence= */ true); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt index eaf541d7b559..76b5d823e0b6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt @@ -88,7 +88,7 @@ constructor( dialog.setPositiveButton( R.string.quick_settings_done, /* onClick = */ null, - /* dismissOnClick = */ true + /* dismissOnClick = */ true, ) } @@ -102,7 +102,7 @@ constructor( labelArray[i] = context.resources.getString( com.android.settingslib.R.string.font_scale_percentage, - (strEntryValues[i].toFloat() * 100).roundToInt() + (strEntryValues[i].toFloat() * 100).roundToInt(), ) } seekBarWithIconButtonsView.setProgressStateLabels(labelArray) @@ -132,7 +132,7 @@ constructor( override fun onUserInteractionFinalized( seekBar: SeekBar, - @ControlUnitType control: Int + @ControlUnitType control: Int, ) { if (control == ControlUnitType.BUTTON) { // The seekbar progress is changed by icon buttons @@ -216,7 +216,7 @@ constructor( !systemSettings.putStringForUser( Settings.System.FONT_SCALE, strEntryValues[lastProgress.get()], - userTracker.userId + userTracker.userId, ) ) { title.post { doneButton.isEnabled = true } @@ -228,13 +228,13 @@ constructor( if ( secureSettings.getStringForUser( Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, - userTracker.userId + userTracker.userId, ) != ON ) { secureSettings.putStringForUser( Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, ON, - userTracker.userId + userTracker.userId, ) } } @@ -249,7 +249,7 @@ constructor( title.setTextSize( TypedValue.COMPLEX_UNIT_PX, - previewConfigContext.resources.getDimension(R.dimen.dialog_title_text_size) + previewConfigContext.resources.getDimension(R.dimen.dialog_title_text_size), ) } diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt new file mode 100644 index 000000000000..2d21d655561f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/activity/data/model/AppVisibilityModel.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 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.activity.data.model + +/** Describes an app's previous and current visibility to the user. */ +data class AppVisibilityModel( + /** True if the app is currently visible to the user and false otherwise. */ + val isAppCurrentlyVisible: Boolean = false, + /** + * The last time this app became visible to the user, in + * [com.android.systemui.util.time.SystemClock.currentTimeMillis] units. Null if the app hasn't + * become visible since the flow started collection. + */ + val lastAppVisibleTime: Long? = null, +) diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt index 94614b70beda..11831eabf19d 100644 --- a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt @@ -18,9 +18,11 @@ package com.android.systemui.activity.data.repository import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND +import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.core.Logger +import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -29,9 +31,23 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.scan /** Repository for interfacing with [ActivityManager]. */ interface ActivityManagerRepository { + + /** + * Given a UID, creates a flow that emits details about when the process with the given UID was + * and is visible to the user. + * + * @param identifyingLogTag a tag identifying who created this flow, used for logging. + */ + fun createAppVisibilityFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<AppVisibilityModel> + /** * Given a UID, creates a flow that emits true when the process with the given UID is visible to * the user and false otherwise. @@ -50,8 +66,38 @@ class ActivityManagerRepositoryImpl @Inject constructor( @Background private val backgroundContext: CoroutineContext, + private val systemClock: SystemClock, private val activityManager: ActivityManager, ) : ActivityManagerRepository { + + override fun createAppVisibilityFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<AppVisibilityModel> { + return createIsAppVisibleFlow(creationUid, logger, identifyingLogTag) + .distinctUntilChanged() + .scan(initial = AppVisibilityModel()) { + oldState: AppVisibilityModel, + newIsVisible: Boolean -> + if (newIsVisible) { + val lastAppVisibleTime = systemClock.currentTimeMillis() + logger.d({ "$str1: Setting lastAppVisibleTime=$long1" }) { + str1 = identifyingLogTag + long1 = lastAppVisibleTime + } + AppVisibilityModel( + isAppCurrentlyVisible = true, + lastAppVisibleTime = lastAppVisibleTime, + ) + } else { + // Reset the current status while maintaining the lastAppVisibleTime + oldState.copy(isAppCurrentlyVisible = false) + } + } + .distinctUntilChanged() + } + override fun createIsAppVisibleFlow( creationUid: Int, logger: Logger, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt index e949dc6a1935..3ef50f68cba4 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/constants/KeyguardBouncerConstants.kt @@ -87,6 +87,14 @@ private fun <T> c(old: T, new: T): T { } } +object PatternBouncerConstants { + object ColorId { + @JvmField val dotColor = colors.materialColorOnSurfaceVariant + @JvmField val activatedDotColor = colors.materialColorOnPrimary + @JvmField val pathColor = colors.materialColorPrimary + } +} + object PinBouncerConstants { @JvmField val pinShapes = c(old = R.array.bouncer_pin_shapes, new = R.array.updated_bouncer_pin_shapes) @@ -126,5 +134,8 @@ object PinBouncerConstants { @JvmField val contractionColorInterpolator = c(old = Interpolators.LINEAR, new = Interpolators.STANDARD)!! + + const val pressedTextScaleX = 1.35f + const val normalTextScaleX = 1.0f } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt index 434a9ce58c3b..7d8945a5b4a7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt @@ -191,7 +191,6 @@ object KeyguardBouncerViewBinder { .filter { it == EXPANSION_VISIBLE } .collect { securityContainerController.onResume(KeyguardSecurityView.SCREEN_ON) - view.announceForAccessibility(securityContainerController.title) } } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java index 82bce0b5338a..257a5a4d9061 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/SeekBarWithIconButtonsView.java @@ -286,7 +286,8 @@ public class SeekBarWithIconButtonsView extends LinearLayout { /** * Notification that the user interaction with SeekBarWithIconButtonsView is finalized. This - * would be triggered after user ends dragging on the slider or clicks icon buttons. + * would be triggered after user ends dragging on the slider or clicks icon buttons. This is + * not called if the progress change was not initiated by the user. * * @param seekBar The SeekBar in which the user ends interaction with * @param control The last user interacted control unit. It would be @@ -318,10 +319,14 @@ public class SeekBarWithIconButtonsView extends LinearLayout { seekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.BUTTON); } else { mOnSeekBarChangeListener.onProgressChanged(seekBar, progress, fromUser); - if (!mSeekByTouch) { + if (!mSeekByTouch && fromUser) { // Accessibility users could change the progress of the seekbar without - // touching the seekbar or clicking the buttons. We will consider the - // interaction has finished in this case. + // touching the seekbar or clicking the buttons. In this, {@code fromUser} + // will be true, and we will consider the interaction to be finished. + // The seekbar progress could be changed when {@code fromUser} is false + // when magnification scale is set by pinch-to-zoom, keyboard control, or + // other services. In this case, we don't need to take finalized actions + // for the progress change. mOnSeekBarChangeListener.onUserInteractionFinalized( seekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER); diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt index 0a9bd4214a12..bf4445ba18db 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt @@ -151,5 +151,7 @@ constructor( override fun instantlyShowOverlay(overlay: OverlayKey) = Unit override fun instantlyHideOverlay(overlay: OverlayKey) = Unit + + override fun freezeAndAnimateToCurrentState() = Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 8bff090959ab..3c68e3a09f02 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -74,7 +74,6 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.dagger.CentralSurfacesModule; import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule; -import com.android.systemui.statusbar.notification.dagger.NotificationStackModule; import com.android.systemui.statusbar.notification.dagger.ReferenceNotificationsModule; import com.android.systemui.statusbar.notification.headsup.HeadsUpModule; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -170,7 +169,6 @@ import javax.inject.Named; WallpaperModule.class, ShortcutHelperModule.class, ContextualEducationModule.class, - NotificationStackModule.class, }) public abstract class ReferenceSystemUIModule { diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index ebe228dab05a..26501596aa1a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -45,7 +45,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // Internal notification backend dependencies crossAppPoliteNotifications dependsOn politeNotifications vibrateWhileUnlockedToken dependsOn politeNotifications - modesUi dependsOn modesApi // Internal notification frontend dependencies NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token @@ -71,9 +70,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha private inline val modesUi get() = FlagToken(android.app.Flags.FLAG_MODES_UI, android.app.Flags.modesUi()) - private inline val modesApi - get() = FlagToken(android.app.Flags.FLAG_MODES_API, android.app.Flags.modesApi()) - private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt index 464201f6ec12..b787fc2a2b17 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyboard.shortcut.data.source import android.content.Context import android.content.res.Resources import android.view.KeyEvent.KEYCODE_D +import android.view.KeyEvent.KEYCODE_DPAD_DOWN import android.view.KeyEvent.KEYCODE_DPAD_LEFT import android.view.KeyEvent.KEYCODE_DPAD_RIGHT import android.view.KeyEvent.KEYCODE_DPAD_UP @@ -73,6 +74,15 @@ constructor(@Main private val resources: Resources, @Application private val con command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_UP) } ) + if (DesktopModeStatus.canEnterDesktopMode(context)) { + // Switch to desktop view + // - Meta + Ctrl + Down arrow + add( + shortcutInfo(resources.getString(R.string.system_multitasking_desktop_view)) { + command(META_META_ON or META_CTRL_ON, KEYCODE_DPAD_DOWN) + } + ) + } if (enableMoveToNextDisplayShortcut()) { // Move a window to the next display: // - Meta + Ctrl + D diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt index 80bdc65f9b97..f69229213690 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LockscreenSceneTransitionRepository.kt @@ -25,11 +25,17 @@ import kotlinx.coroutines.flow.MutableStateFlow class LockscreenSceneTransitionRepository @Inject constructor() { /** - * This [KeyguardState] will indicate which sub state within KTF should be navigated to when the - * next transition into the Lockscreen scene is started. It will be consumed exactly once and - * after that the state will be set back to [DEFAULT_STATE]. + * This [KeyguardState] will indicate which sub-state within KTF should be navigated to next. + * + * This can be either starting a transition to the `Lockscreen` scene or cancelling a transition + * from the `Lockscreen` scene and returning back to it. + * + * A `null` value means that no explicit target state was set and therefore the [DEFAULT_STATE] + * should be used. + * + * Once consumed, this state should be reset to `null`. */ - val nextLockscreenTargetState: MutableStateFlow<KeyguardState> = MutableStateFlow(DEFAULT_STATE) + val nextLockscreenTargetState: MutableStateFlow<KeyguardState?> = MutableStateFlow(null) companion object { val DEFAULT_STATE = KeyguardState.LOCKSCREEN diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt index bf0f25ff089e..a3796ab5ee27 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt @@ -118,26 +118,19 @@ constructor( powerInteractor.isAwake, keyguardInteractor.isAodAvailable, communalSceneInteractor.isIdleOnCommunal, - communalInteractor.editModeOpen, + keyguardInteractor.isDreaming, keyguardInteractor.isKeyguardOccluded, ) .filterRelevantKeyguardStateAnd { - (isAlternateBouncerShowing, isPrimaryBouncerShowing, _, _, _) -> + (isAlternateBouncerShowing, isPrimaryBouncerShowing, _, _, _, _) -> !isAlternateBouncerShowing && !isPrimaryBouncerShowing } - .collect { - ( - _, - _, - isAwake, - isAodAvailable, - isIdleOnCommunal, - isCommunalEditMode, - isOccluded) -> + .collect { (_, _, isAwake, isAodAvailable, isIdleOnCommunal, isDreaming, isOccluded) + -> // When unlocking over glanceable hub to enter edit mode, transitioning directly // to GONE prevents the lockscreen flash. Let listenForAlternateBouncerToGone // handle it. - if (isCommunalEditMode) return@collect + if (communalInteractor.editModeOpen.value) return@collect val hubV2 = communalSettingsInteractor.isV2FlagEnabled() val to = if (!isAwake) { @@ -150,8 +143,10 @@ constructor( if (!hubV2 && isIdleOnCommunal) { if (SceneContainerFlag.isEnabled) return@collect KeyguardState.GLANCEABLE_HUB - } else if (isOccluded) { + } else if (isOccluded && !isDreaming) { KeyguardState.OCCLUDED + } else if (hubV2 && isDreaming) { + KeyguardState.DREAMING } else if (hubV2 && isIdleOnCommunal) { if (SceneContainerFlag.isEnabled) return@collect KeyguardState.GLANCEABLE_HUB diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 0700ec639153..6f5f662d6fa3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -159,7 +159,6 @@ constructor( val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value - val canStartDreaming = dreamManager.canStartDreaming(false) if (!deviceEntryInteractor.isLockscreenEnabled()) { if (!SceneContainerFlag.isEnabled) { @@ -192,13 +191,6 @@ constructor( if (!SceneContainerFlag.isEnabled) { transitionToGlanceableHub() } - } else if (canStartDreaming) { - // If we're waking up to dream, transition directly to dreaming without - // showing the lockscreen. - startTransitionTo( - KeyguardState.DREAMING, - ownerReason = "moving from doze to dream", - ) } else { startTransitionTo(KeyguardState.LOCKSCREEN) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index 673fa9730c53..63cf4f72e415 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -24,7 +24,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardClockRepository import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.ClockSizeSetting -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 @@ -33,12 +32,13 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarou import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockId import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor import com.android.systemui.util.kotlin.combine +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -61,7 +61,7 @@ constructor( mediaCarouselInteractor: MediaCarouselInteractor, activeNotificationsInteractor: ActiveNotificationsInteractor, aodPromotedNotificationInteractor: AODPromotedNotificationInteractor, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, @@ -70,8 +70,13 @@ constructor( private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, ) { private val isOnAod: Flow<Boolean> = - keyguardTransitionInteractor.currentKeyguardState.map { it == KeyguardState.AOD } + keyguardTransitionInteractor.currentKeyguardState.map { it == AOD } + /** + * The clock size setting explicitly selected by the user. When it is `SMALL`, the large clock + * is never shown. When it is `DYNAMIC`, the clock size gets determined based on a combination + * of system signals. + */ val selectedClockSize: StateFlow<ClockSizeSetting> = keyguardClockRepository.selectedClockSize val currentClockId: Flow<ClockId> = keyguardClockRepository.currentClockId @@ -103,36 +108,46 @@ constructor( activeNotificationsInteractor.areAnyNotificationsPresent } - val clockSize: StateFlow<ClockSize> = + private val dynamicClockSize: Flow<ClockSize> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.isShadeLayoutWide, - areAnyNotificationsPresent, - mediaCarouselInteractor.hasActiveMediaOrRecommendation, - keyguardInteractor.isDozing, - isOnAod, - ) { isShadeLayoutWide, hasNotifs, hasMedia, isDozing, isOnAod -> - return@combine when { - keyguardClockRepository.shouldForceSmallClock && !isOnAod -> ClockSize.SMALL - !isShadeLayoutWide && (hasNotifs || hasMedia) -> ClockSize.SMALL - !isShadeLayoutWide -> ClockSize.LARGE - hasMedia && !isDozing -> ClockSize.SMALL - else -> ClockSize.LARGE - } + shadeModeInteractor.isShadeLayoutWide, + areAnyNotificationsPresent, + mediaCarouselInteractor.hasActiveMediaOrRecommendation, + keyguardInteractor.isDozing, + isOnAod, + ) { isShadeLayoutWide, hasNotifs, hasMedia, isDozing, isOnAod -> + when { + keyguardClockRepository.shouldForceSmallClock && !isOnAod -> ClockSize.SMALL + !isShadeLayoutWide && (hasNotifs || hasMedia) -> ClockSize.SMALL + !isShadeLayoutWide -> ClockSize.LARGE + hasMedia && !isDozing -> ClockSize.SMALL + else -> ClockSize.LARGE } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = ClockSize.LARGE, - ) + } } else { keyguardClockRepository.clockSize } + val clockSize: StateFlow<ClockSize> = + selectedClockSize + .flatMapLatestConflated { selectedSize -> + if (selectedSize == ClockSizeSetting.SMALL) { + flowOf(ClockSize.SMALL) + } else { + dynamicClockSize + } + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ClockSize.LARGE, + ) + val clockShouldBeCentered: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, areAnyNotificationsPresent, isAodPromotedNotificationPresent, isOnAod, @@ -156,7 +171,7 @@ constructor( } } else { combine( - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, areAnyNotificationsPresent, isAodPromotedNotificationPresent, keyguardInteractor.dozeTransitionModel, @@ -203,7 +218,7 @@ constructor( val renderedClockId: ClockId get() { - return clock?.let { clock -> clock.config.id } + return clock?.config?.id ?: run { Log.e(TAG, "No clock is available") "MISSING_CLOCK_ID" 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 5f821022d580..1b70ff84f09d 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 @@ -119,7 +119,8 @@ constructor( } else { val targetState = if (idle.currentScene == Scenes.Lockscreen) { - transitionInteractor.startedKeyguardTransitionStep.value.from + repository.nextLockscreenTargetState.value + ?: transitionInteractor.startedKeyguardTransitionStep.value.from } else { UNDEFINED } @@ -197,11 +198,11 @@ constructor( TransitionInfo( ownerName = this::class.java.simpleName, from = UNDEFINED, - to = repository.nextLockscreenTargetState.value, + to = repository.nextLockscreenTargetState.value ?: DEFAULT_STATE, animator = null, modeOnCanceled = TransitionModeOnCanceled.RESET, ) - repository.nextLockscreenTargetState.value = DEFAULT_STATE + repository.nextLockscreenTargetState.value = null startTransition(newTransition) } @@ -215,7 +216,7 @@ constructor( animator = null, modeOnCanceled = TransitionModeOnCanceled.RESET, ) - repository.nextLockscreenTargetState.value = DEFAULT_STATE + repository.nextLockscreenTargetState.value = null startTransition(newTransition) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 8e385385b8c4..da87e38daa9b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -370,6 +370,14 @@ object KeyguardRootViewBinder { repeatOnLifecycle(Lifecycle.State.STARTED) { if (wallpaperFocalAreaViewModel.hasFocalArea.value) { launch { + wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds.collect { + wallpaperFocalAreaBounds -> + wallpaperFocalAreaViewModel.setFocalAreaBounds( + wallpaperFocalAreaBounds + ) + } + } + launch { wallpaperFocalAreaViewModel.wallpaperFocalAreaBounds .filterNotNull() .collect { wallpaperFocalAreaViewModel.setFocalAreaBounds(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index aed86648e3cf..0a087404c075 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -26,7 +26,6 @@ import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.BurnInModel -import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.StateToValue @@ -35,6 +34,7 @@ import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlin.math.max import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -51,6 +51,7 @@ import kotlinx.coroutines.flow.stateIn * Models UI state for elements that need to apply anti-burn-in tactics when showing in AOD * (always-on display). */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class AodBurnInViewModel @Inject @@ -184,10 +185,9 @@ constructor( keyguardClockViewModel.currentClock.value ?.config ?.useAlternateSmartspaceAODTransition == true - // Only scale large non-weather clocks - // elements in large weather clock will translate the same as smartspace - val useScaleOnly = - (!useAltAod) && keyguardClockViewModel.clockSize.value == ClockSize.LARGE + // Only scale large non-weather clocks elements in large weather clock will translate + // the same as smartspace + val useScaleOnly = (!useAltAod) && keyguardClockViewModel.isLargeClockVisible.value val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt() val translationY = max(params.topInset - params.minViewY, burnInY) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt index 9018c58a7e36..e6a85c6860c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModel.kt @@ -39,6 +39,4 @@ constructor(animationFlow: KeyguardTransitionAnimationFlow) { ) val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) - // Notifications should not be shown while transitioning to dream. - val notificationAlpha = transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 88fdc83fa7a0..cf5cc264be8d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context import android.content.res.Resources -import androidx.annotation.VisibleForTesting import androidx.constraintlayout.helper.widget.Layer import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.customization.R as customR @@ -27,11 +26,10 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize -import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.plugins.clocks.ClockPreviewConfig import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.ui.SystemBarUtilsProxy import javax.inject.Inject @@ -47,11 +45,11 @@ import kotlinx.coroutines.flow.stateIn class KeyguardClockViewModel @Inject constructor( - val context: Context, + private val context: Context, keyguardClockInteractor: KeyguardClockInteractor, @Application private val applicationScope: CoroutineScope, aodNotificationIconViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, - @get:VisibleForTesting val shadeInteractor: ShadeInteractor, + private val shadeModeInteractor: ShadeModeInteractor, private val systemBarUtils: SystemBarUtilsProxy, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, // TODO: b/374267505 - Use ShadeDisplayAware resources here. @@ -59,17 +57,7 @@ constructor( ) { var burnInLayer: Layer? = null - val clockSize: StateFlow<ClockSize> = - combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) { - selectedSize, - clockSize -> - if (selectedSize == ClockSizeSetting.SMALL) ClockSize.SMALL else clockSize - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = ClockSize.LARGE, - ) + val clockSize: StateFlow<ClockSize> = keyguardClockInteractor.clockSize val isLargeClockVisible: StateFlow<Boolean> = clockSize @@ -118,7 +106,7 @@ constructor( combine( isLargeClockVisible, clockShouldBeCentered, - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, currentClock, ) { isLargeClockVisible, clockShouldBeCentered, isShadeLayoutWide, currentClock -> if (currentClock?.config?.useCustomClockScene == true) { @@ -163,7 +151,7 @@ constructor( fun getSmallClockTopMargin(): Int { return ClockPreviewConfig( context, - shadeInteractor.isShadeLayoutWide.value, + shadeModeInteractor.isShadeLayoutWide.value, SceneContainerFlag.isEnabled, ) .getSmallClockTopPadding(systemBarUtils.getStatusBarHeaderHeightKeyguard()) @@ -172,7 +160,7 @@ constructor( val smallClockTopMargin = combine( configurationInteractor.onAnyConfigurationChange, - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, ) { _, _ -> getSmallClockTopMargin() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt index ba03c48c65e9..e70d696a207f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,6 +32,7 @@ class KeyguardMediaViewModel constructor( mediaCarouselInteractor: MediaCarouselInteractor, keyguardInteractor: KeyguardInteractor, + shadeModeInteractor: ShadeModeInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("KeyguardMediaViewModel.hydrator") @@ -54,6 +56,12 @@ constructor( mediaCarouselInteractor.hasActiveMediaOrRecommendation.value, ) + val isShadeLayoutWide: Boolean by + hydrator.hydratedStateOf( + traceName = "isShadeLayoutWide", + source = shadeModeInteractor.isShadeLayoutWide, + ) + override suspend fun onActivated(): Nothing { hydrator.activate() } 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 3e3a89a55f66..ecebaee62862 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 @@ -17,8 +17,9 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Resources +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor @@ -30,86 +31,121 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallback import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallbackDelegator import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.res.R -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn class LockscreenContentViewModel @AssistedInject constructor( - clockInteractor: KeyguardClockInteractor, - private val interactor: KeyguardBlueprintInteractor, + private val clockInteractor: KeyguardClockInteractor, + interactor: KeyguardBlueprintInteractor, private val authController: AuthController, val touchHandling: KeyguardTouchHandlingViewModel, - private val shadeInteractor: ShadeInteractor, - private val unfoldTransitionInteractor: UnfoldTransitionInteractor, - private val deviceEntryInteractor: DeviceEntryInteractor, - private val transitionInteractor: KeyguardTransitionInteractor, + shadeModeInteractor: ShadeModeInteractor, + unfoldTransitionInteractor: UnfoldTransitionInteractor, + deviceEntryInteractor: DeviceEntryInteractor, + transitionInteractor: KeyguardTransitionInteractor, private val keyguardTransitionAnimationCallbackDelegator: KeyguardTransitionAnimationCallbackDelegator, @Assisted private val keyguardTransitionAnimationCallback: KeyguardTransitionAnimationCallback, ) : ExclusiveActivatable() { - @VisibleForTesting val clockSize = clockInteractor.clockSize + + private val hydrator = Hydrator("LockscreenContentViewModel.hydrator") val isUdfpsVisible: Boolean get() = authController.isUdfpsSupported - val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide + /** Where to place the notifications stack on the lockscreen. */ + val notificationsPlacement: NotificationsPlacement by + hydrator.hydratedStateOf( + traceName = "notificationsPlacement", + initialValue = NotificationsPlacement.BelowClock, + source = + combine(shadeModeInteractor.shadeMode, clockInteractor.clockSize) { + shadeMode, + clockSize -> + if (shadeMode is ShadeMode.Split) { + NotificationsPlacement.BesideClock(alignment = Alignment.TopEnd) + } else if (clockSize == ClockSize.SMALL) { + NotificationsPlacement.BelowClock + } else { + NotificationsPlacement.BesideClock(alignment = Alignment.TopStart) + } + }, + ) - private val _unfoldTranslations = MutableStateFlow(UnfoldTranslations()) /** Amount of horizontal translation that should be applied to elements in the scene. */ - val unfoldTranslations: StateFlow<UnfoldTranslations> = _unfoldTranslations.asStateFlow() + val unfoldTranslations: UnfoldTranslations by + hydrator.hydratedStateOf( + traceName = "unfoldTranslations", + initialValue = UnfoldTranslations(), + source = + combine( + unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), + unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), + ::UnfoldTranslations, + ), + ) - private val _isContentVisible = MutableStateFlow(true) /** Whether the content of the scene UI should be shown. */ - val isContentVisible: StateFlow<Boolean> = _isContentVisible.asStateFlow() + val isContentVisible: Boolean by + hydrator.hydratedStateOf( + traceName = "isContentVisible", + initialValue = true, + // Content is visible unless we're OCCLUDED. Currently, we don't have nice animations + // into and out of OCCLUDED, so the lockscreen/AOD content is hidden immediately upon + // entering/exiting OCCLUDED. + source = transitionInteractor.transitionValue(KeyguardState.OCCLUDED).map { it == 0f }, + ) + + /** Indicates whether lockscreen notifications should be rendered. */ + val areNotificationsVisible: Boolean by + hydrator.hydratedStateOf( + traceName = "areNotificationsVisible", + initialValue = false, + // Content is visible unless we're OCCLUDED. Currently, we don't have nice animations + // into and out of OCCLUDED, so the lockscreen/AOD content is hidden immediately upon + // entering/exiting OCCLUDED. + source = + combine(clockInteractor.clockSize, shadeModeInteractor.isShadeLayoutWide) { + clockSize, + isShadeLayoutWide -> + clockSize == ClockSize.SMALL || isShadeLayoutWide + }, + ) /** @see DeviceEntryInteractor.isBypassEnabled */ - val isBypassEnabled: StateFlow<Boolean> - get() = deviceEntryInteractor.isBypassEnabled + val isBypassEnabled: Boolean by + hydrator.hydratedStateOf( + traceName = "isBypassEnabled", + source = deviceEntryInteractor.isBypassEnabled, + ) + + val blueprintId: String by + hydrator.hydratedStateOf( + traceName = "blueprintId", + initialValue = interactor.getCurrentBlueprint().id, + source = interactor.blueprint.map { it.id }.distinctUntilChanged(), + ) override suspend fun onActivated(): Nothing { coroutineScope { try { + launch { hydrator.activate() } + keyguardTransitionAnimationCallbackDelegator.delegate = keyguardTransitionAnimationCallback - launch { - combine( - unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), - unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), - ) { start, end -> - UnfoldTranslations(start = start, end = end) - } - .collect { _unfoldTranslations.value = it } - } - - launch { - transitionInteractor - .transitionValue(KeyguardState.OCCLUDED) - .map { it > 0f } - .collect { fullyOrPartiallyOccluded -> - // Content is visible unless we're OCCLUDED. Currently, we don't have - // nice - // animations into and out of OCCLUDED, so the lockscreen/AOD content is - // hidden immediately upon entering/exiting OCCLUDED. - _isContentVisible.value = !fullyOrPartiallyOccluded - } - } awaitCancellation() } finally { @@ -118,16 +154,8 @@ constructor( } } - /** Returns a flow that indicates whether lockscreen notifications should be rendered. */ - fun areNotificationsVisible(): Flow<Boolean> { - return combine(clockSize, shadeInteractor.isShadeLayoutWide) { clockSize, isShadeLayoutWide - -> - clockSize == ClockSize.SMALL || isShadeLayoutWide - } - } - fun getSmartSpacePaddingTop(resources: Resources): Int { - return if (clockSize.value == ClockSize.LARGE) { + return if (clockInteractor.clockSize.value == ClockSize.LARGE) { resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) + resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) } else { @@ -135,17 +163,6 @@ constructor( } } - fun blueprintId(scope: CoroutineScope): StateFlow<String> { - return interactor.blueprint - .map { it.id } - .distinctUntilChanged() - .stateIn( - scope = scope, - started = SharingStarted.WhileSubscribed(), - initialValue = interactor.getCurrentBlueprint().id, - ) - } - data class UnfoldTranslations( /** @@ -162,6 +179,15 @@ constructor( val end: Float = 0f, ) + /** Where to place the notifications stack on the lockscreen. */ + sealed interface NotificationsPlacement { + /** Show notifications below the lockscreen clock. */ + data object BelowClock : NotificationsPlacement + + /** Show notifications side-by-side with the clock. */ + data class BesideClock(val alignment: Alignment) : NotificationsPlacement + } + @AssistedFactory interface Factory { fun create( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt index c3182bf7a320..1466d8b4288e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt @@ -143,6 +143,12 @@ interface MediaDataManager { * place immediately. */ override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {} + + /** + * Called whenever the current active media notification changes. Should only be used if + * [SceneContainerFlag] is disabled + */ + override fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {} } companion object { 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 1464849156dc..59f98d83e149 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 @@ -1434,6 +1434,9 @@ class MediaDataProcessor( * place immediately. */ fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {} + + /** Called whenever the current active media notification changes */ + fun onCurrentActiveMediaChanged(key: String?, data: MediaData?) {} } /** 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 173b600de06b..93c4bafe4273 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 @@ -87,6 +87,7 @@ import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTE import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY as SSPACE_CARD_REPORTED__DREAM_OVERLAY import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN as SSPACE_CARD_REPORTED__LOCKSCREEN import com.android.systemui.shared.system.SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE +import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -155,6 +156,7 @@ constructor( private val mediaCarouselViewModel: MediaCarouselViewModel, private val mediaViewControllerFactory: Provider<MediaViewController>, private val deviceEntryInteractor: DeviceEntryInteractor, + private val mediaControlChipInteractor: MediaControlChipInteractor, ) : Dumpable { /** The current width of the carousel */ var currentCarouselWidth: Int = 0 @@ -957,6 +959,9 @@ constructor( } } mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) MediaPlayerData.updateVisibleMediaPlayers() // Automatically scroll to the active player if needed if (shouldScrollToKey) { @@ -1015,6 +1020,9 @@ constructor( ) updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) mediaFrame.requiresRemeasuring = true onUiExecutionEnd?.run() } @@ -1023,6 +1031,9 @@ constructor( updatePlayer(key, data, isSsReactivated, curVisibleMediaKey, existingPlayer) updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) mediaFrame.requiresRemeasuring = true onUiExecutionEnd?.run() } @@ -1036,6 +1047,9 @@ constructor( } updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) mediaFrame.requiresRemeasuring = true onUiExecutionEnd?.run() } @@ -1194,6 +1208,9 @@ constructor( mediaContent.removeView(removed.recommendationViewHolder?.recommendations) removed.onDestroy() mediaCarouselScrollHandler.onPlayersChanged() + mediaControlChipInteractor.updateMediaControlChipModelLegacy( + MediaPlayerData.getFirstActiveMediaData() + ) updatePageIndicator() if (dismissMediaData) { @@ -1928,6 +1945,16 @@ internal object MediaPlayerData { fun visiblePlayerKeys() = visibleMediaPlayers.values + /** Returns the [MediaData] associated with the first mediaPlayer in the mediaCarousel. */ + fun getFirstActiveMediaData(): MediaData? { + mediaPlayers.entries.forEach { entry -> + if (!entry.key.isSsMediaRec && entry.key.data.active) { + return entry.key.data + } + } + return null + } + /** Returns the index of the first non-timeout media. */ fun firstActiveMediaIndex(): Int { mediaPlayers.entries.forEachIndexed { index, e -> diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index c1778119a3fd..2b36872dbe36 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -1040,13 +1040,19 @@ constructor( expandedLayout.load(context, R.xml.media_recommendations_expanded) } } - readjustPlayPauseWidth() + readjustUIUpdateConstraints() refreshState() } - private fun readjustPlayPauseWidth() { + private fun readjustUIUpdateConstraints() { // TODO: move to xml file when flag is removed. if (Flags.mediaControlsUiUpdate()) { + collapsedLayout.setGuidelineEnd( + R.id.action_button_guideline, + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_collapsed_guideline + ), + ) collapsedLayout.constrainWidth( R.id.actionPlayPause, context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt index 709723fa9480..6022b7b1fc13 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/util/MediaViewModelListUpdateCallback.kt @@ -18,6 +18,7 @@ package com.android.systemui.media.controls.ui.util import androidx.recyclerview.widget.ListUpdateCallback import com.android.systemui.media.controls.ui.viewmodel.MediaCommonViewModel +import kotlin.math.min /** A [ListUpdateCallback] to apply media events needed to reach the new state. */ class MediaViewModelListUpdateCallback( @@ -46,7 +47,7 @@ class MediaViewModelListUpdateCallback( } override fun onChanged(position: Int, count: Int, payload: Any?) { - for (i in position until position + count) { + for (i in position until min(position + count, new.size)) { onUpdated(new[i], position) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java index f5e62323e769..c58ba377fb68 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java @@ -20,23 +20,16 @@ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECT import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; -import android.annotation.DrawableRes; -import android.annotation.StringRes; import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; +import android.text.TextUtils; import android.util.Log; import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; import androidx.annotation.DoNotInline; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import androidx.core.widget.CompoundButtonCompat; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.annotations.VisibleForTesting; @@ -49,23 +42,64 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** - * Adapter for media output dialog. + * A parent RecyclerView adapter for the media output dialog device list. This class doesn't + * manipulate the layout directly. */ -public class MediaOutputAdapter extends MediaOutputBaseAdapter { +public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> { + record OngoingSessionStatus(boolean host) {} - private static final String TAG = "MediaOutputAdapter"; + record GroupStatus(Boolean selected, Boolean deselectable) {} + + enum ConnectionState { + CONNECTED, + CONNECTING, + DISCONNECTED, + } + + protected final MediaSwitchingController mController; + private int mCurrentActivePosition; + private boolean mIsDragging; + private static final String TAG = "MediaOutputAdapterBase"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final float DEVICE_DISABLED_ALPHA = 0.5f; - private static final float DEVICE_ACTIVE_ALPHA = 1f; - protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); + protected final List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>(); private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping(); - public MediaOutputAdapter(MediaSwitchingController controller) { - super(controller); + public MediaOutputAdapterBase(MediaSwitchingController controller) { + mController = controller; + mCurrentActivePosition = -1; + mIsDragging = false; setHasStableIds(true); } - @Override + boolean isCurrentlyConnected(MediaDevice device) { + return TextUtils.equals(device.getId(), + mController.getCurrentConnectedMediaDevice().getId()) + || (mController.getSelectedMediaDevice().size() == 1 + && isDeviceIncluded(mController.getSelectedMediaDevice(), device)); + } + + boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) { + for (MediaDevice device : deviceList) { + if (TextUtils.equals(device.getId(), targetDevice.getId())) { + return true; + } + } + return false; + } + + boolean isDragging() { + return mIsDragging; + } + + void setIsDragging(boolean isDragging) { + mIsDragging = isDragging; + } + + int getCurrentActivePosition() { + return mCurrentActivePosition; + } + + /** Refreshes the RecyclerView dataset and forces re-render. */ public void updateItems() { mMediaItemList.clear(); mMediaItemList.addAll(mController.getMediaItemList()); @@ -79,47 +113,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, - int viewType) { - super.onCreateViewHolder(viewGroup, viewType); - switch (viewType) { - case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: - return new MediaGroupDividerViewHolder(mHolderView); - case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: - case MediaItem.MediaItemType.TYPE_DEVICE: - default: - return new MediaDeviceViewHolder(mHolderView); - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (position >= mMediaItemList.size()) { - if (DEBUG) { - Log.d(TAG, "Incorrect position: " + position + " list size: " - + mMediaItemList.size()); - } - return; - } - MediaItem currentMediaItem = mMediaItemList.get(position); - switch (currentMediaItem.getMediaItemType()) { - case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: - ((MediaGroupDividerViewHolder) viewHolder).onBind(currentMediaItem.getTitle()); - break; - case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: - ((MediaDeviceViewHolder) viewHolder).onBindPairNewDevice(); - break; - case MediaItem.MediaItemType.TYPE_DEVICE: - ((MediaDeviceViewHolder) viewHolder).onBind( - currentMediaItem, - position); - break; - default: - Log.d(TAG, "Incorrect position: " + position); - } - } - - @Override public long getItemId(int position) { if (position >= mMediaItemList.size()) { Log.d(TAG, "Incorrect position for item id: " + position); @@ -145,15 +138,17 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { return mMediaItemList.size(); } - class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder { + abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder { + + Context mContext; - MediaDeviceViewHolder(View view) { + MediaDeviceViewHolderBase(View view, Context context) { super(view); + mContext = context; } - void onBind(MediaItem mediaItem, int position) { + void renderItem(MediaItem mediaItem, int position) { MediaDevice device = mediaItem.getMediaDevice().get(); - super.onBind(device, position); boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice(); final boolean currentlyConnected = isCurrentlyConnected(device); boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device); @@ -198,7 +193,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { if (mCurrentActivePosition == position) { mCurrentActivePosition = -1; } - mItemLayout.setVisibility(View.VISIBLE); if (mController.isAnyDeviceTransferring()) { if (device.getState() == MediaDeviceState.STATE_CONNECTING) { @@ -265,37 +259,15 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } } - private void renderDeviceItem(boolean hideGroupItem, MediaDevice device, + protected abstract void renderDeviceItem(boolean hideGroupItem, MediaDevice device, ConnectionState connectionState, boolean restrictVolumeAdjustment, GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus, View.OnClickListener clickListener, boolean deviceDisabled, String subtitle, - Drawable deviceStatusIcon) { - if (hideGroupItem) { - mItemLayout.setVisibility(View.GONE); - return; - } - updateTitle(device.getName()); - updateTitleIcon(device, connectionState, restrictVolumeAdjustment); - updateSeekBar(device, connectionState, restrictVolumeAdjustment, - getDeviceItemContentDescription(device)); - updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus); - updateLoadingIndicator(connectionState); - updateFullItemClickListener(clickListener); - updateContentAlpha(deviceDisabled); - updateSubtitle(subtitle); - updateDeviceStatusIcon(deviceStatusIcon); - updateItemBackground(connectionState); - } + Drawable deviceStatusIcon); - private void renderDeviceGroupItem() { - String sessionName = mController.getSessionName() == null ? "" - : mController.getSessionName().toString(); - updateTitle(sessionName); - updateUnmutedVolumeIcon(null /* device */); - updateGroupSeekBar(getGroupItemContentDescription(sessionName)); - updateEndAreaForDeviceGroup(); - updateItemBackground(ConnectionState.CONNECTED); - } + protected abstract void renderDeviceGroupItem(); + + protected abstract void disableSeekBar(); private OngoingSessionStatus getOngoingSessionStatus(MediaDevice device) { return device.hasOngoingSession() ? new OngoingSessionStatus( @@ -322,95 +294,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { return !mController.getSelectableMediaDevice().isEmpty(); } - /** Renders the right side round pill button / checkbox. */ - private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState, - @Nullable GroupStatus groupStatus, - @Nullable OngoingSessionStatus ongoingSessionStatus) { - boolean showEndArea = false; - boolean isCheckbox = false; - // If both group status and the ongoing session status are present, only the ongoing - // session controls are displayed. The current layout design doesn't allow both group - // and ongoing session controls to be rendered simultaneously. - if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) { - showEndArea = true; - updateEndAreaForOngoingSession(device, ongoingSessionStatus.host()); - } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) { - showEndArea = true; - isCheckbox = true; - updateEndAreaForGroupCheckBox(device, groupStatus); - } - updateEndAreaVisibility(showEndArea, isCheckbox); - } - - private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) { - if (Flags.enableOutputSwitcherDeviceGrouping()) { - return isGroupCheckboxEnabled(groupStatus); - } - return true; - } - - private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) { - boolean disabled = groupStatus.selected() && !groupStatus.deselectable(); - return !disabled; - } - - public void setCheckBoxColor(CheckBox checkBox, int color) { - int[][] states = {{android.R.attr.state_checked}, {}}; - int[] colors = {color, color}; - CompoundButtonCompat.setButtonTintList(checkBox, new - ColorStateList(states, colors)); - } - - private void updateContentAlpha(boolean deviceDisabled) { - float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA; - mTitleIcon.setAlpha(alphaValue); - mTitleText.setAlpha(alphaValue); - mSubTitleText.setAlpha(alphaValue); - mStatusIcon.setAlpha(alphaValue); - } - - private void updateEndAreaForDeviceGroup() { - updateEndAreaWithIcon( - v -> { - mShouldGroupSelectedMediaItems = false; - notifyDataSetChanged(); - }, - R.drawable.media_output_item_expand_group, - R.string.accessibility_expand_group); - updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */); - } - - private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) { - updateEndAreaWithIcon( - v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v), - isHost ? R.drawable.media_output_status_edit_session - : R.drawable.ic_sound_bars_anim, - R.string.accessibility_open_application); - } - - private void updateEndAreaWithIcon(View.OnClickListener clickListener, - @DrawableRes int iconDrawableId, - @StringRes int accessibilityStringId) { - updateEndAreaColor(mController.getColorSeekbarProgress()); - mEndClickIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); - mEndClickIcon.setOnClickListener(clickListener); - mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick()); - Drawable drawable = mContext.getDrawable(iconDrawableId); - mEndClickIcon.setImageDrawable(drawable); - if (drawable instanceof AnimatedVectorDrawable) { - ((AnimatedVectorDrawable) drawable).start(); - } - if (Flags.enableOutputSwitcherDeviceGrouping()) { - mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId)); - } - } - - public void updateEndAreaColor(int color) { - mEndTouchArea.setBackgroundTintList( - ColorStateList.valueOf(color)); - } - @Nullable private View.OnClickListener getClickListenerBasedOnSelectionBehavior( @NonNull MediaDevice device) { @@ -427,57 +310,12 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } } - void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) { - if (deviceStatusIcon == null) { - mStatusIcon.setVisibility(View.GONE); - } else { - mStatusIcon.setImageDrawable(deviceStatusIcon); - mStatusIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); - if (deviceStatusIcon instanceof AnimatedVectorDrawable) { - ((AnimatedVectorDrawable) deviceStatusIcon).start(); - } - mStatusIcon.setVisibility(View.VISIBLE); - } - } - - public void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device, - @NonNull GroupStatus groupStatus) { - boolean isEnabled = isGroupCheckboxEnabled(groupStatus); - mEndTouchArea.setOnClickListener( - isEnabled ? (v) -> mCheckBox.performClick() : null); - mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress() - : mController.getColorItemBackground()); - mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device)); - mCheckBox.setOnCheckedChangeListener(null); - mCheckBox.setChecked(groupStatus.selected()); - mCheckBox.setOnCheckedChangeListener( - isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered( - !groupStatus.selected(), device) : null); - mCheckBox.setEnabled(isEnabled); - setCheckBoxColor(mCheckBox, mController.getColorItemContent()); - } - - private void updateFullItemClickListener(@Nullable View.OnClickListener listener) { - mContainerLayout.setOnClickListener(listener); - updateIconAreaClickListener(listener); - } - - /** Binds a ViewHolder for a "Connect a device" item. */ - void onBindPairNewDevice() { - mTitleText.setTextColor(mController.getColorItemContent()); - mCheckBox.setVisibility(View.GONE); - updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new)); - updateItemBackground(ConnectionState.DISCONNECTED); - final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add); - mTitleIcon.setImageDrawable(addDrawable); - mTitleIcon.setImageTintList( - ColorStateList.valueOf(mController.getColorItemContent())); - mContainerLayout.setOnClickListener(mController::launchBluetoothPairing); + protected void onExpandGroupButtonClicked() { + mShouldGroupSelectedMediaItems = false; + notifyDataSetChanged(); } - private void onGroupActionTriggered(boolean isChecked, MediaDevice device) { + protected void onGroupActionTriggered(boolean isChecked, MediaDevice device) { disableSeekBar(); if (isChecked && isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { mController.addDeviceToPlayMedia(device); @@ -523,32 +361,18 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { notifyDataSetChanged(); } - private String getDeviceItemContentDescription(@NonNull MediaDevice device) { + protected String getDeviceItemContentDescription(@NonNull MediaDevice device) { return mContext.getString( device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE ? R.string.accessibility_bluetooth_name : R.string.accessibility_cast_name, device.getName()); } - private String getGroupItemContentDescription(String sessionName) { + protected String getGroupItemContentDescription(String sessionName) { return mContext.getString(R.string.accessibility_cast_name, sessionName); } } - class MediaGroupDividerViewHolder extends RecyclerView.ViewHolder { - final TextView mTitleText; - - MediaGroupDividerViewHolder(@NonNull View itemView) { - super(itemView); - mTitleText = itemView.requireViewById(R.id.title); - } - - void onBind(String groupDividerTitle) { - mTitleText.setTextColor(mController.getColorItemContent()); - mTitleText.setText(groupDividerTitle); - } - } - @RequiresApi(34) private static class Api34Impl { @DoNotInline diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java index f97b3d3d5e38..565b2e41f75a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java @@ -18,14 +18,18 @@ package com.android.systemui.media.dialog; import android.animation.Animator; import android.animation.ValueAnimator; -import android.app.WallpaperColors; +import android.annotation.DrawableRes; +import android.annotation.StringRes; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.ClipDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.LayerDrawable; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -40,6 +44,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.widget.CompoundButtonCompat; import androidx.recyclerview.widget.RecyclerView; import com.android.media.flags.Flags; @@ -48,82 +53,67 @@ import com.android.settingslib.media.MediaDevice; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.res.R; -import java.util.List; - /** - * Base adapter for media output dialog. + * A RecyclerView adapter for the legacy UI media output dialog device list. */ -public abstract class MediaOutputBaseAdapter extends - RecyclerView.Adapter<RecyclerView.ViewHolder> { - - record OngoingSessionStatus(boolean host) {} - - record GroupStatus(Boolean selected, Boolean deselectable) {} - - enum ConnectionState { - CONNECTED, - CONNECTING, - DISCONNECTED, - } - - protected final MediaSwitchingController mController; +public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase { + private static final String TAG = "MediaOutputAdapterL"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int UNMUTE_DEFAULT_VOLUME = 2; - - Context mContext; + private static final float DEVICE_DISABLED_ALPHA = 0.5f; + private static final float DEVICE_ACTIVE_ALPHA = 1f; View mHolderView; - boolean mIsDragging; - int mCurrentActivePosition; private boolean mIsInitVolumeFirstTime; - public MediaOutputBaseAdapter(MediaSwitchingController controller) { - mController = controller; - mIsDragging = false; - mCurrentActivePosition = -1; + public MediaOutputAdapterLegacy(MediaSwitchingController controller) { + super(controller); mIsInitVolumeFirstTime = true; } - /** - * Refresh current dataset - */ - public abstract void updateItems(); - @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { - mContext = viewGroup.getContext(); - mHolderView = LayoutInflater.from(mContext).inflate(MediaItem.getMediaLayoutId(viewType), - viewGroup, false); - return null; - } - - void updateColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) { - mController.setCurrentColorScheme(wallpaperColors, isDarkTheme); - } + Context context = viewGroup.getContext(); + mHolderView = LayoutInflater.from(viewGroup.getContext()).inflate( + MediaItem.getMediaLayoutId(viewType), + viewGroup, false); - boolean isCurrentlyConnected(MediaDevice device) { - return TextUtils.equals(device.getId(), - mController.getCurrentConnectedMediaDevice().getId()) - || (mController.getSelectedMediaDevice().size() == 1 - && isDeviceIncluded(mController.getSelectedMediaDevice(), device)); + switch (viewType) { + case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: + return new MediaGroupDividerViewHolderLegacy(mHolderView); + case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: + case MediaItem.MediaItemType.TYPE_DEVICE: + default: + return new MediaDeviceViewHolderLegacy(mHolderView, context); + } } - boolean isDeviceIncluded(List<MediaDevice> deviceList, MediaDevice targetDevice) { - for (MediaDevice device : deviceList) { - if (TextUtils.equals(device.getId(), targetDevice.getId())) { - return true; + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (position >= getItemCount()) { + if (DEBUG) { + Log.d(TAG, "Incorrect position: " + position + " list size: " + + getItemCount()); } + return; + } + MediaItem currentMediaItem = mMediaItemList.get(position); + switch (currentMediaItem.getMediaItemType()) { + case MediaItem.MediaItemType.TYPE_GROUP_DIVIDER: + ((MediaGroupDividerViewHolderLegacy) viewHolder).onBind( + currentMediaItem.getTitle()); + break; + case MediaItem.MediaItemType.TYPE_PAIR_NEW_DEVICE: + ((MediaDeviceViewHolderLegacy) viewHolder).onBindPairNewDevice(); + break; + case MediaItem.MediaItemType.TYPE_DEVICE: + ((MediaDeviceViewHolderLegacy) viewHolder).onBindDevice(currentMediaItem, position); + break; + default: + Log.d(TAG, "Incorrect position: " + position); } - return false; - } - - boolean isDragging() { - return mIsDragging; - } - - int getCurrentActivePosition() { - return mCurrentActivePosition; } public MediaSwitchingController getController() { @@ -133,7 +123,7 @@ public abstract class MediaOutputBaseAdapter extends /** * ViewHolder for binding device view. */ - abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder { + class MediaDeviceViewHolderLegacy extends MediaDeviceViewHolderBase { private static final int ANIM_DURATION = 500; @@ -158,8 +148,8 @@ public abstract class MediaOutputBaseAdapter extends private ValueAnimator mVolumeAnimator; private int mLatestUpdateVolume = -1; - MediaDeviceBaseViewHolder(View view) { - super(view); + MediaDeviceViewHolderLegacy(View view, Context context) { + super(view, context); mContainerLayout = view.requireViewById(R.id.device_container); mItemLayout = view.requireViewById(R.id.item_layout); mTitleText = view.requireViewById(R.id.title); @@ -180,8 +170,10 @@ public abstract class MediaOutputBaseAdapter extends initAnimator(); } - void onBind(MediaDevice device, int position) { + void onBindDevice(MediaItem mediaItem, int position) { + MediaDevice device = mediaItem.getMediaDevice().get(); mDeviceId = device.getId(); + mItemLayout.setVisibility(View.VISIBLE); mCheckBox.setVisibility(View.GONE); mStatusIcon.setVisibility(View.GONE); mEndTouchArea.setVisibility(View.GONE); @@ -196,6 +188,54 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setProgressTintList( ColorStateList.valueOf(mController.getColorSeekbarProgress())); enableFocusPropertyForView(mContainerLayout); + renderItem(mediaItem, position); + } + + /** Binds a ViewHolder for a "Connect a device" item. */ + void onBindPairNewDevice() { + mTitleText.setTextColor(mController.getColorItemContent()); + mCheckBox.setVisibility(View.GONE); + updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new)); + updateItemBackground(ConnectionState.DISCONNECTED); + final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add); + mTitleIcon.setImageDrawable(addDrawable); + mTitleIcon.setImageTintList( + ColorStateList.valueOf(mController.getColorItemContent())); + mContainerLayout.setOnClickListener(mController::launchBluetoothPairing); + } + + @Override + protected void renderDeviceItem(boolean hideGroupItem, MediaDevice device, + ConnectionState connectionState, boolean restrictVolumeAdjustment, + GroupStatus groupStatus, OngoingSessionStatus ongoingSessionStatus, + View.OnClickListener clickListener, boolean deviceDisabled, String subtitle, + Drawable deviceStatusIcon) { + if (hideGroupItem) { + mItemLayout.setVisibility(View.GONE); + return; + } + updateTitle(device.getName()); + updateTitleIcon(device, connectionState, restrictVolumeAdjustment); + updateSeekBar(device, connectionState, restrictVolumeAdjustment, + getDeviceItemContentDescription(device)); + updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus); + updateLoadingIndicator(connectionState); + updateFullItemClickListener(clickListener); + updateContentAlpha(deviceDisabled); + updateSubtitle(subtitle); + updateDeviceStatusIcon(deviceStatusIcon); + updateItemBackground(connectionState); + } + + @Override + protected void renderDeviceGroupItem() { + String sessionName = mController.getSessionName() == null ? "" + : mController.getSessionName().toString(); + updateTitle(sessionName); + updateUnmutedVolumeIcon(null /* device */); + updateGroupSeekBar(getGroupItemContentDescription(sessionName)); + updateEndAreaForDeviceGroup(); + updateItemBackground(ConnectionState.CONNECTED); } void updateTitle(CharSequence title) { @@ -303,7 +343,7 @@ public abstract class MediaOutputBaseAdapter extends private void initializeSeekbarVolume( @Nullable MediaDevice device, int currentVolume, boolean isCurrentSeekbarInvisible) { - if (!mIsDragging) { + if (!isDragging()) { if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1 || currentVolume == mLatestUpdateVolume)) { // Update only if volume of device and value of volume bar doesn't match. @@ -459,6 +499,132 @@ public abstract class MediaOutputBaseAdapter extends : R.drawable.media_output_icon_volume; } + private void updateContentAlpha(boolean deviceDisabled) { + float alphaValue = deviceDisabled ? DEVICE_DISABLED_ALPHA : DEVICE_ACTIVE_ALPHA; + mTitleIcon.setAlpha(alphaValue); + mTitleText.setAlpha(alphaValue); + mSubTitleText.setAlpha(alphaValue); + mStatusIcon.setAlpha(alphaValue); + } + + private void updateDeviceStatusIcon(@Nullable Drawable deviceStatusIcon) { + if (deviceStatusIcon == null) { + mStatusIcon.setVisibility(View.GONE); + } else { + mStatusIcon.setImageDrawable(deviceStatusIcon); + mStatusIcon.setImageTintList( + ColorStateList.valueOf(mController.getColorItemContent())); + if (deviceStatusIcon instanceof AnimatedVectorDrawable) { + ((AnimatedVectorDrawable) deviceStatusIcon).start(); + } + mStatusIcon.setVisibility(View.VISIBLE); + } + } + + + /** Renders the right side round pill button / checkbox. */ + private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState, + @Nullable GroupStatus groupStatus, + @Nullable OngoingSessionStatus ongoingSessionStatus) { + boolean showEndArea = false; + boolean isCheckbox = false; + // If both group status and the ongoing session status are present, only the ongoing + // session controls are displayed. The current layout design doesn't allow both group + // and ongoing session controls to be rendered simultaneously. + if (ongoingSessionStatus != null && connectionState == ConnectionState.CONNECTED) { + showEndArea = true; + updateEndAreaForOngoingSession(device, ongoingSessionStatus.host()); + } else if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) { + showEndArea = true; + isCheckbox = true; + updateEndAreaForGroupCheckBox(device, groupStatus); + } + updateEndAreaVisibility(showEndArea, isCheckbox); + } + + private void updateEndAreaForDeviceGroup() { + updateEndAreaWithIcon( + v -> { + onExpandGroupButtonClicked(); + }, + R.drawable.media_output_item_expand_group, + R.string.accessibility_expand_group); + updateEndAreaVisibility(true /* showEndArea */, false /* isCheckbox */); + } + + private void updateEndAreaForOngoingSession(@NonNull MediaDevice device, boolean isHost) { + updateEndAreaWithIcon( + v -> mController.tryToLaunchInAppRoutingIntent(device.getId(), v), + isHost ? R.drawable.media_output_status_edit_session + : R.drawable.ic_sound_bars_anim, + R.string.accessibility_open_application); + } + + private void updateEndAreaWithIcon(View.OnClickListener clickListener, + @DrawableRes int iconDrawableId, + @StringRes int accessibilityStringId) { + updateEndAreaColor(mController.getColorSeekbarProgress()); + mEndClickIcon.setImageTintList( + ColorStateList.valueOf(mController.getColorItemContent())); + mEndClickIcon.setOnClickListener(clickListener); + mEndTouchArea.setOnClickListener(v -> mEndClickIcon.performClick()); + Drawable drawable = mContext.getDrawable(iconDrawableId); + mEndClickIcon.setImageDrawable(drawable); + if (drawable instanceof AnimatedVectorDrawable) { + ((AnimatedVectorDrawable) drawable).start(); + } + if (Flags.enableOutputSwitcherDeviceGrouping()) { + mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId)); + } + } + + private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device, + @NonNull GroupStatus groupStatus) { + boolean isEnabled = isGroupCheckboxEnabled(groupStatus); + mEndTouchArea.setOnClickListener( + isEnabled ? (v) -> mCheckBox.performClick() : null); + mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress() + : mController.getColorItemBackground()); + mEndTouchArea.setContentDescription(getDeviceItemContentDescription(device)); + mCheckBox.setOnCheckedChangeListener(null); + mCheckBox.setChecked(groupStatus.selected()); + mCheckBox.setOnCheckedChangeListener( + isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered( + !groupStatus.selected(), device) : null); + mCheckBox.setEnabled(isEnabled); + setCheckBoxColor(mCheckBox, mController.getColorItemContent()); + } + + private void setCheckBoxColor(CheckBox checkBox, int color) { + int[][] states = {{android.R.attr.state_checked}, {}}; + int[] colors = {color, color}; + CompoundButtonCompat.setButtonTintList(checkBox, new + ColorStateList(states, colors)); + } + + private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) { + if (Flags.enableOutputSwitcherDeviceGrouping()) { + return isGroupCheckboxEnabled(groupStatus); + } + return true; + } + + private boolean isGroupCheckboxEnabled(@NonNull GroupStatus groupStatus) { + boolean disabled = groupStatus.selected() && !groupStatus.deselectable(); + return !disabled; + } + + private void updateEndAreaColor(int color) { + mEndTouchArea.setBackgroundTintList( + ColorStateList.valueOf(color)); + } + + private void updateFullItemClickListener(@Nullable View.OnClickListener listener) { + mContainerLayout.setOnClickListener(listener); + updateIconAreaClickListener(listener); + } + void updateIconAreaClickListener(@Nullable View.OnClickListener listener) { mIconAreaLayout.setOnClickListener(listener); } @@ -498,6 +664,7 @@ public abstract class MediaOutputBaseAdapter extends }); } + @Override protected void disableSeekBar() { mSeekBar.setEnabled(false); mSeekBar.setOnTouchListener((v, event) -> true); @@ -589,7 +756,7 @@ public abstract class MediaOutputBaseAdapter extends int currentVolume = MediaOutputSeekbar.scaleProgressToVolume( seekBar.getProgress()); mStartFromMute = (currentVolume == 0); - mIsDragging = true; + setIsDragging(true); } @Override @@ -604,11 +771,25 @@ public abstract class MediaOutputBaseAdapter extends } mTitleIcon.setVisibility(View.VISIBLE); mVolumeValueText.setVisibility(View.GONE); - mIsDragging = false; + setIsDragging(false); } protected boolean shouldHandleProgressChanged() { return mMediaDevice != null; } }; } + + class MediaGroupDividerViewHolderLegacy extends RecyclerView.ViewHolder { + final TextView mTitleText; + + MediaGroupDividerViewHolderLegacy(@NonNull View itemView) { + super(itemView); + mTitleText = itemView.requireViewById(R.id.title); + } + + void onBind(String groupDividerTitle) { + mTitleText.setTextColor(mController.getColorItemContent()); + mTitleText.setText(groupDividerTitle); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java index 64256f97fd78..d791361d555f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java @@ -105,7 +105,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog private boolean mIsLeBroadcastCallbackRegistered; private boolean mDismissing; - MediaOutputBaseAdapter mAdapter; + MediaOutputAdapterBase mAdapter; protected Executor mExecutor; @@ -342,7 +342,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap()); colorSetUpdated = !wallpaperColors.equals(mWallpaperColors); if (colorSetUpdated) { - mAdapter.updateColorScheme(wallpaperColors, isDarkThemeOn); + mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn); updateButtonBackgroundColorFilter(); updateDialogBackgroundColor(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java index 9b5b872a00db..9ade9e275ca1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java @@ -245,7 +245,7 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { broadcastSender, mediaSwitchingController, /* includePlaybackAndAppMetadata */ true); - mAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class // that extends MediaOutputBaseDialog if (!aboveStatusbar) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java index c9af7b322811..2e602be4556e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java @@ -53,7 +53,7 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata); mDialogTransitionAnimator = dialogTransitionAnimator; mUiEventLogger = uiEventLogger; - mAdapter = new MediaOutputAdapter(mMediaSwitchingController); + mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController); if (!aboveStatusbar) { getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } diff --git a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt index a0663d72a076..e293e202633e 100644 --- a/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt +++ b/packages/SystemUI/src/com/android/systemui/modes/shared/ModesUi.kt @@ -25,7 +25,7 @@ object ModesUi { /** Is the refactor enabled */ @JvmStatic inline val isEnabled - get() = Flags.modesApi() && Flags.modesUi() + get() = Flags.modesUi() /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt index c7b165415aea..c43c1a999fcb 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt @@ -20,12 +20,15 @@ import androidx.compose.runtime.getValue import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel +import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.awaitCancellation @@ -33,6 +36,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOf /** * Models UI state used to render the content of the notifications shade overlay. @@ -47,6 +51,8 @@ constructor( val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory, val sceneInteractor: SceneInteractor, private val shadeInteractor: ShadeInteractor, + disableFlagsInteractor: DisableFlagsInteractor, + mediaCarouselInteractor: MediaCarouselInteractor, activeNotificationsInteractor: ActiveNotificationsInteractor, ) : ExclusiveActivatable() { @@ -69,6 +75,22 @@ constructor( ), ) + val showMedia: Boolean by + hydrator.hydratedStateOf( + traceName = "showMedia", + initialValue = + disableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled() && + mediaCarouselInteractor.hasActiveMediaOrRecommendation.value, + source = + disableFlagsInteractor.disableFlags.flatMapLatestConflated { + if (it.isQuickSettingsEnabled()) { + mediaCarouselInteractor.hasActiveMediaOrRecommendation + } else { + flowOf(false) + } + }, + ) + override suspend fun onActivated(): Nothing { coroutineScope { launch { hydrator.activate() } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt index 57d40638b8df..9117afb1de6f 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt @@ -39,6 +39,11 @@ import androidx.annotation.DrawableRes import androidx.annotation.WorkerThread import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_COLLAPSE +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_EXPAND +import androidx.core.view.accessibility.AccessibilityViewCommand +import com.android.systemui.Flags import com.android.systemui.animation.ViewHierarchyAnimator import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog @@ -282,49 +287,95 @@ class PrivacyDialogV2( val expandToggle = itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!! - expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down) expandToggle.visibility = View.VISIBLE - - ViewCompat.replaceAccessibilityAction( - itemCard, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - context.getString(R.string.privacy_dialog_expand_action), - null, - ) - val expandedLayout = itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!! expandedLayout.setOnClickListener { // Stop clicks from propagating } - itemCard.setOnClickListener { - if (expandedLayout.visibility == View.VISIBLE) { - expandedLayout.visibility = View.GONE - expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down) - ViewCompat.replaceAccessibilityAction( - it!!, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - context.getString(R.string.privacy_dialog_expand_action), - null, - ) - } else { - expandedLayout.visibility = View.VISIBLE - expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up) - ViewCompat.replaceAccessibilityAction( - it!!, - AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, - context.getString(R.string.privacy_dialog_collapse_action), - null, - ) + if (Flags.expandCollapsePrivacyDialog()) { + updateExpansion(ACTION_COLLAPSE, itemCard, expandedLayout, expandToggle) + + itemCard.setOnClickListener { + if (expandedLayout.visibility == View.VISIBLE) { + updateExpansion(ACTION_COLLAPSE, it!!, expandedLayout, expandToggle) + } else { + updateExpansion(ACTION_EXPAND, it!!, expandedLayout, expandToggle) + } } - ViewHierarchyAnimator.animateNextUpdate( - rootView = window!!.decorView, - excludedViews = setOf(expandedLayout), + } else { + expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down) + ViewCompat.replaceAccessibilityAction( + itemCard, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.privacy_dialog_expand_action), + null, ) + + itemCard.setOnClickListener { + if (expandedLayout.visibility == View.VISIBLE) { + expandedLayout.visibility = View.GONE + expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down) + ViewCompat.replaceAccessibilityAction( + it!!, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.privacy_dialog_expand_action), + null, + ) + } else { + expandedLayout.visibility = View.VISIBLE + expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up) + ViewCompat.replaceAccessibilityAction( + it!!, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.privacy_dialog_collapse_action), + null, + ) + } + ViewHierarchyAnimator.animateNextUpdate( + rootView = window!!.decorView, + excludedViews = setOf(expandedLayout), + ) + } } } + private fun updateExpansion( + newState: AccessibilityActionCompat, + itemCard: View, + expandedLayout: View, + expandToggle: ImageView, + ) { + expandedLayout.visibility = if (newState == ACTION_COLLAPSE) View.GONE else View.VISIBLE + expandToggle.setImageResource( + if (newState == ACTION_COLLAPSE) R.drawable.privacy_dialog_expand_toggle_down + else R.drawable.privacy_dialog_expand_toggle_up + ) + val accessibilityString = + context.getString( + if (newState == ACTION_COLLAPSE) R.string.privacy_dialog_expand_action + else R.string.privacy_dialog_collapse_action + ) + ViewCompat.replaceAccessibilityAction( + itemCard, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + accessibilityString, + null, + ) + val expandCollapseAccessibilityListener = + AccessibilityViewCommand { view: View, _: AccessibilityViewCommand.CommandArguments? -> + view.callOnClick() + } + ViewCompat.replaceAccessibilityAction( + itemCard, + if (newState == ACTION_COLLAPSE) ACTION_EXPAND else ACTION_COLLAPSE, + accessibilityString, + expandCollapseAccessibilityListener, + ) + ViewCompat.removeAccessibilityAction(itemCard, newState.id) + } + private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) { indicatorIcon.setTint(getForegroundColor(active)) val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt index c9a0635021da..61a8fa3d2a6e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt @@ -55,6 +55,7 @@ object SubtitleArrayMapping { subtitleIdsMap["font_scaling"] = R.array.tile_states_font_scaling subtitleIdsMap["hearing_devices"] = R.array.tile_states_hearing_devices subtitleIdsMap["notes"] = R.array.tile_states_notes + subtitleIdsMap["desktopeffects"] = R.array.tile_states_desktopeffects } /** Get the subtitle resource id of the given tile */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java index c6bcab48fa68..75cb8ddca484 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java @@ -589,8 +589,10 @@ public class InternetDialogDelegateLegacy implements } mSecondaryMobileNetworkLayout = mDialogView.findViewById( R.id.secondary_mobile_network_layout); - mSecondaryMobileNetworkLayout.setOnClickListener( - this::onClickConnectedSecondarySub); + if (mCanConfigMobileData) { + mSecondaryMobileNetworkLayout.setOnClickListener( + this::onClickConnectedSecondarySub); + } mSecondaryMobileNetworkLayout.setBackground(mBackgroundOn); TextView mSecondaryMobileTitleText = mDialogView.requireViewById( @@ -623,6 +625,8 @@ public class InternetDialogDelegateLegacy implements mDialogView.requireViewById(R.id.secondary_settings_icon); mSecondaryMobileSettingsIcon.setColorFilter( dialog.getContext().getColor(R.color.connected_network_primary_color)); + mSecondaryMobileSettingsIcon.setVisibility(mCanConfigMobileData ? + View.VISIBLE : View.INVISIBLE); // set secondary visual for default data sub mMobileNetworkLayout.setBackground(mBackgroundOff); diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index caa7bbae0420..e357f63479dc 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -163,4 +163,12 @@ constructor( fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) { _transitionState.value = transitionState } + + /** + * If currently in a transition between contents, cancel that transition and go back to the + * pre-transition state. + */ + fun freezeAndAnimateToCurrentState() { + dataSource.freezeAndAnimateToCurrentState() + } } 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 e9e7deca0abf..01180859b1d2 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 @@ -234,6 +234,10 @@ constructor( * The change is animated. Therefore, it will be some time before the UI will switch to the * desired scene. Once enough of the transition has occurred, the [currentScene] will become * [toScene] (unless the transition is canceled by user action or another call to this method). + * + * If [forceSettleToTargetScene] is `true` and the target scene is the same as the current + * scene, any current transition will be canceled and an animation to the target scene will be + * started. */ @JvmOverloads fun changeScene( @@ -241,9 +245,19 @@ constructor( loggingReason: String, transitionKey: TransitionKey? = null, sceneState: Any? = null, + forceSettleToTargetScene: Boolean = false, ) { val currentSceneKey = currentScene.value val resolvedScene = sceneFamilyResolvers.get()[toScene]?.resolvedScene?.value ?: toScene + + if (resolvedScene == currentSceneKey && forceSettleToTargetScene) { + logger.logSceneChangeCancellation(scene = resolvedScene, sceneState = sceneState) + onSceneAboutToChangeListener.forEach { + it.onSceneAboutToChange(resolvedScene, sceneState) + } + repository.freezeAndAnimateToCurrentState() + } + if ( !validateSceneChange( from = currentSceneKey, @@ -523,14 +537,32 @@ constructor( } if (from == to) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${from.debugName} is the same as ${to.debugName}", + ) return false } if (to !in repository.allContentKeys) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} isn't present in allContentKeys", + ) return false } if (disabledContentInteractor.isDisabled(to)) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is currently disabled", + ) return false } @@ -580,14 +612,58 @@ constructor( } if (to != null && disabledContentInteractor.isDisabled(to)) { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is currently disabled", + ) return false } - val isFromValid = (from == null) || (from in currentOverlays.value) - val isToValid = - (to == null) || (to !in currentOverlays.value && to in repository.allContentKeys) + return when { + to != null && from != null && to == from -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${from.debugName} is the same as ${to.debugName}", + ) + false + } - return isFromValid && isToValid && from != to + to != null && to !in repository.allContentKeys -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is not in allContentKeys", + ) + false + } + + from != null && from !in currentOverlays.value -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${from.debugName} is not a current overlay", + ) + false + } + + to != null && to in currentOverlays.value -> { + logger.logSceneChangeRejection( + from = from, + to = to, + originalChangeReason = loggingReason, + rejectionReason = "${to.debugName} is already a current overlay", + ) + false + } + + else -> true + } } /** Returns a flow indicating if the currently visible scene can be resolved from [family]. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt index 140b231593bd..aab37d433e4f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt @@ -16,7 +16,6 @@ package com.android.systemui.scene.domain.resolver -import android.util.Log import com.android.compose.animation.scene.SceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -84,7 +83,7 @@ constructor( isDreamingWithOverlay: Boolean, isAbleToDream: Boolean, ): SceneKey { - val result = when { + return when { // Dream can run even if Keyguard is disabled, thus it has the highest priority here. isDreamingWithOverlay && isAbleToDream -> Scenes.Dream !isKeyguardEnabled -> Scenes.Gone @@ -93,21 +92,9 @@ constructor( !isUnlocked -> Scenes.Lockscreen else -> Scenes.Gone } - Log.d(TAG, "homeScene emitting $result, values:") - Log.d(TAG, " isKeyguardEnabled=$isKeyguardEnabled") - Log.d(TAG, " canSwipeToEnter=$canSwipeToEnter") - Log.d(TAG, " isDeviceEntered=$isDeviceEntered" ) - Log.d(TAG, " isUnlocked=$isUnlocked") - Log.d(TAG, " isDreamingWithOverlay=$isDreamingWithOverlay") - Log.d(TAG, " isAbleToDream=$isAbleToDream") - Log.d(TAG, "") - return result } companion object { - - private const val TAG = "HomeSceneFamilyResolver" - val homeScenes = setOf( Scenes.Gone, 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 94e32fcb9ac6..218ad477c45e 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 @@ -90,6 +90,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -553,6 +554,7 @@ constructor( targetSceneKey = Scenes.Lockscreen, loggingReason = "device is starting to sleep", sceneState = keyguardInteractor.asleepKeyguardState.value, + freezeAndAnimateToCurrentState = true, ) } else { val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value @@ -610,15 +612,24 @@ constructor( private fun handleShadeTouchability() { applicationScope.launch { - shadeInteractor.isShadeTouchable - .distinctUntilChanged() - .filter { !it } - .collect { - switchToScene( - targetSceneKey = Scenes.Lockscreen, - loggingReason = "device became non-interactive (SceneContainerStartable)", - ) + repeatWhen(deviceEntryInteractor.isDeviceEntered.map { !it }) { + // Run logic only when the device isn't entered. + repeatWhen( + sceneInteractor.transitionState.map { !it.isTransitioning(to = Scenes.Gone) } + ) { + // Run logic only when not transitioning to gone. + shadeInteractor.isShadeTouchable + .distinctUntilChanged() + .filter { !it } + .collect { + switchToScene( + targetSceneKey = Scenes.Lockscreen, + loggingReason = + "device became non-interactive (SceneContainerStartable)", + ) + } } + } } } @@ -923,11 +934,13 @@ constructor( targetSceneKey: SceneKey, loggingReason: String, sceneState: Any? = null, + freezeAndAnimateToCurrentState: Boolean = false, ) { sceneInteractor.changeScene( toScene = targetSceneKey, loggingReason = loggingReason, sceneState = sceneState, + forceSettleToTargetScene = freezeAndAnimateToCurrentState, ) } @@ -1013,6 +1026,14 @@ constructor( } } + private suspend fun repeatWhen(condition: Flow<Boolean>, block: suspend () -> Unit) { + condition.distinctUntilChanged().collectLatest { conditionMet -> + if (conditionMet) { + block() + } + } + } + companion object { private const val TAG = "SceneContainerStartable" } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt index d00585858ccb..73c71f6088e1 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene.shared.logger +import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey @@ -74,6 +75,50 @@ class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: ) } + fun logSceneChangeCancellation(scene: SceneKey, sceneState: Any?) { + logBuffer.log( + tag = TAG, + level = LogLevel.INFO, + messageInitializer = { + str1 = scene.debugName + str2 = sceneState?.toString() + }, + messagePrinter = { "CANCELED scene change. scene: $str1, sceneState: $str2" }, + ) + } + + fun logSceneChangeRejection( + from: ContentKey?, + to: ContentKey?, + originalChangeReason: String, + rejectionReason: String, + ) { + logBuffer.log( + tag = TAG, + level = LogLevel.INFO, + messageInitializer = { + str1 = "${from?.debugName ?: "<none>"} → ${to?.debugName ?: "<none>"}" + str2 = rejectionReason + str3 = originalChangeReason + bool1 = to is OverlayKey + }, + messagePrinter = { + buildString { + append("REJECTED ") + append( + if (bool1) { + "overlay " + } else { + "scene " + } + ) + append("change $str1 because \"$str2\" ") + append("(original change reason: \"$str3\")") + } + }, + ) + } + fun logSceneTransition(transitionState: ObservableTransitionState) { when (transitionState) { is ObservableTransitionState.Transition -> { diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt index daf2d7f698b6..42c4b24a72d3 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt @@ -83,4 +83,10 @@ interface SceneDataSource { /** Asks for [overlay] to be instantly hidden, without an animated transition of any kind. */ fun instantlyHideOverlay(overlay: OverlayKey) + + /** + * If currently in a transition between contents, cancel that transition and go back to the + * pre-transition state. + */ + fun freezeAndAnimateToCurrentState() } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt index dcb699539760..d6dce38d0bbf 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt @@ -82,6 +82,10 @@ class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneCo delegateMutable.value.instantlyHideOverlay(overlay) } + override fun freezeAndAnimateToCurrentState() { + delegateMutable.value.freezeAndAnimateToCurrentState() + } + /** * Binds the current, dependency injection provided [SceneDataSource] to the given object. * @@ -120,5 +124,7 @@ class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneCo override fun instantlyShowOverlay(overlay: OverlayKey) = Unit override fun instantlyHideOverlay(overlay: OverlayKey) = Unit + + override fun freezeAndAnimateToCurrentState() = Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index f926d39760fe..96b224fbd4f3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -42,12 +42,12 @@ import com.android.systemui.shade.display.ShadeDisplayPolicyModule import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor -import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround +import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider +import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHiderImpl import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.phone.ConfigurationForwarder import com.android.systemui.statusbar.policy.ConfigurationController -import dagger.BindsOptionalOf import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey @@ -67,7 +67,7 @@ import javax.inject.Qualifier * By using this dedicated module, we ensure the notification shade window always utilizes the * correct display context and resources, regardless of the display it's on. */ -@Module(includes = [OptionalShadeDisplayAwareBindings::class, ShadeDisplayPolicyModule::class]) +@Module(includes = [ShadeDisplayPolicyModule::class]) object ShadeDisplayAwareModule { /** Creates a new context for the shade window. */ @@ -242,17 +242,6 @@ object ShadeDisplayAwareModule { } } - @Provides - @IntoMap - @ClassKey(ShadeDisplaysInteractor::class) - fun provideShadeDisplaysInteractor(impl: Provider<ShadeDisplaysInteractor>): CoreStartable { - return if (ShadeWindowGoesAround.isEnabled) { - impl.get() - } else { - CoreStartable.NOP - } - } - /** * Provided for making classes easier to test. In tests, a custom method to wait for the next * frame can be easily provided. @@ -264,11 +253,25 @@ object ShadeDisplayAwareModule { fun provideShadeOnDefaultDisplayWhenLocked(): Boolean = true } +/** Module that should be included only if the shade window [WindowRootView] is available. */ @Module -internal interface OptionalShadeDisplayAwareBindings { - @BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView +object ShadeDisplayAwareWithShadeWindowModule { + @Provides + @IntoMap + @ClassKey(ShadeDisplaysInteractor::class) + fun provideShadeDisplaysInteractor(impl: Provider<ShadeDisplaysInteractor>): CoreStartable { + return if (ShadeWindowGoesAround.isEnabled) { + impl.get() + } else { + CoreStartable.NOP + } + } - @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor + @Provides + @SysUISingleton + fun bindNotificationStackRebindingHider( + impl: NotificationStackRebindingHiderImpl + ): NotificationStackRebindingHider = impl } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt index 13b540aa54ba..5fda998dac2d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt @@ -24,8 +24,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.data.repository.ShadeDisplaysRepository -import com.android.systemui.util.kotlin.getOrNull -import java.util.Optional import java.util.concurrent.CancellationException import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -51,22 +49,13 @@ import kotlinx.coroutines.withTimeout class ShadeDisplayChangeLatencyTracker @Inject constructor( - optionalShadeRootView: Optional<WindowRootView>, + private val shadeRootView: WindowRootView, @ShadeDisplayAware private val configurationRepository: ConfigurationRepository, private val latencyTracker: LatencyTracker, @Background private val bgScope: CoroutineScope, private val choreographerUtils: ChoreographerUtils, ) { - private val shadeRootView = - optionalShadeRootView.getOrNull() - ?: error( - """ - ShadeRootView must be provided for ShadeDisplayChangeLatencyTracker to work. - If it is not, it means this is being instantiated in a SystemUI variant that shouldn't. - """ - .trimIndent() - ) /** * We need to keep this always up to date eagerly to avoid delays receiving the new display ID. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 7d4b0ed6304c..c44e066aad3a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -54,7 +54,12 @@ import javax.inject.Provider /** Module for classes related to the notification shade. */ @Module( includes = - [StartShadeModule::class, ShadeViewProviderModule::class, WindowRootViewBlurModule::class] + [ + StartShadeModule::class, + ShadeViewProviderModule::class, + WindowRootViewBlurModule::class, + ShadeDisplayAwareWithShadeWindowModule::class, + ] ) abstract class ShadeModule { companion object { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index e746274a39c1..9a5c96824e77 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -39,9 +39,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotif import com.android.systemui.statusbar.notification.row.NotificationRebindingTracker import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider import com.android.systemui.statusbar.phone.ConfigurationForwarder -import com.android.systemui.util.kotlin.getOrNull import com.android.window.flags.Flags -import java.util.Optional import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.seconds @@ -63,17 +61,14 @@ constructor( @Background private val bgScope: CoroutineScope, @Main private val mainThreadContext: CoroutineContext, private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker, - shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>, + private val shadeExpandedInteractor: ShadeExpandedStateInteractor, private val shadeExpansionIntent: ShadeExpansionIntent, private val activeNotificationsInteractor: ActiveNotificationsInteractor, private val notificationRebindingTracker: NotificationRebindingTracker, - notificationStackRebindingHider: Optional<NotificationStackRebindingHider>, + private val notificationStackRebindingHider: NotificationStackRebindingHider, @ShadeDisplayAware private val configForwarder: ConfigurationForwarder, ) : CoreStartable { - private val shadeExpandedInteractor = requireOptional(shadeExpandedInteractor) - private val notificationStackRebindingHider = requireOptional(notificationStackRebindingHider) - private val hasActiveNotifications: Boolean get() = activeNotificationsInteractor.areAnyNotificationsPresentValue @@ -224,24 +219,5 @@ constructor( const val TAG = "ShadeDisplaysInteractor" const val COLLAPSE_EXPAND_REASON = "Shade window move" val TIMEOUT = 1.seconds - - /** - * [ShadeDisplaysInteractor] is bound in the SystemUI module for all variants, but needs - * some specific dependencies to be bound from each variant (e.g. - * [ShadeExpandedStateInteractor] or [NotificationStackRebindingHider]). When those are not - * bound, this class is not expected to be instantiated, and trying to instantiate it would - * crash. - */ - inline fun <reified T> requireOptional(optional: Optional<T>): T { - return optional.getOrNull() - ?: error( - """ - ${T::class.java.simpleName} must be provided for ShadeDisplaysInteractor to work. - If it is not, it means this is being instantiated in a SystemUI variant that - shouldn't. - """ - .trimIndent() - ) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index 9d81be2091c2..e8b5d5bdf7df 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade.domain.interactor -import android.util.Log import com.android.app.tracing.coroutines.flow.flowName import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** The non-empty [ShadeInteractor] implementation. */ @@ -100,31 +98,17 @@ constructor( override val isShadeTouchable: Flow<Boolean> = combine( - powerInteractor.isAsleep.onEach { - Log.d(TAG, "isShadeTouchable: upstream isAsleep=$it") - }, - keyguardTransitionInteractor - .isInTransition(Edge.create(to = KeyguardState.AOD)) - .onEach { Log.d(TAG, "isShadeTouchable: upstream isTransitioningToAod=$it") }, - keyguardRepository.dozeTransitionModel - .map { it.to == DozeStateModel.DOZE_PULSING } - .onEach { Log.d(TAG, "isShadeTouchable: upstream isPulsing=$it") }, + powerInteractor.isAsleep, + keyguardTransitionInteractor.isInTransition(Edge.create(to = KeyguardState.AOD)), + keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING }, ) { isAsleep, isTransitioningToAod, isPulsing -> - val downstream = - when { - // If the device is transitioning to AOD, only accept touches if - // still animating. - isTransitioningToAod -> dozeParams.shouldControlScreenOff() - // If the device is asleep, only accept touches if there's a pulse - isAsleep -> isPulsing - else -> true - } - Log.d(TAG, "isShadeTouchable emitting $downstream, values:") - Log.d(TAG, " isAsleep=$isAsleep") - Log.d(TAG, " isTransitioningToAod=$isTransitioningToAod") - Log.d(TAG, " isPulsing=$isPulsing") - Log.d(TAG, "") - downstream + when { + // If the device is transitioning to AOD, only accept touches if still animating. + isTransitioningToAod -> dozeParams.shouldControlScreenOff() + // If the device is asleep, only accept touches if there's a pulse + isAsleep -> isPulsing + else -> true + } } override val isExpandToQsEnabled: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt index 8f4e8701cad8..1ab0b93da175 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.scene.domain.SceneFrameworkTableLog +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository @@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn /** @@ -89,10 +91,14 @@ constructor( ) : ShadeModeInteractor { private val isDualShadeEnabled: Flow<Boolean> = - secureSettingsRepository.boolSetting( - Settings.Secure.DUAL_SHADE, - defaultValue = DUAL_SHADE_ENABLED_DEFAULT, - ) + if (SceneContainerFlag.isEnabled) { + secureSettingsRepository.boolSetting( + Settings.Secure.DUAL_SHADE, + defaultValue = DUAL_SHADE_ENABLED_DEFAULT, + ) + } else { + flowOf(false) + } override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide diff --git a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt index c23ff5302b3c..dc444ffc2a34 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/shared/flag/ShadeWindowGoesAround.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.shared.flag +import android.window.DesktopExperienceFlags import com.android.systemui.Flags import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils @@ -30,10 +31,26 @@ object ShadeWindowGoesAround { val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) + /** + * This is defined as [DesktopExperienceFlags] to make it possible to enable it together with + * all the other desktop experience flags from the dev settings. + * + * Alternatively, using adb: + * ```bash + * adb shell aflags enable com.android.window.flags.show_desktop_experience_dev_option && \ + * adb shell setprop persist.wm.debug.desktop_experience_devopts 1 + * ``` + */ + val FLAG = + DesktopExperienceFlags.DesktopExperienceFlag( + Flags::shadeWindowGoesAround, + /* shouldOverrideByDevOption= */ true, + ) + /** Is the refactor enabled */ @JvmStatic inline val isEnabled: Boolean - get() = Flags.shadeWindowGoesAround() + get() = FLAG.isTrue /** * Called to ensure code is only run when the flag is enabled. This protects users from the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt index b1af811178e4..d8c3e2546a8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.notification.domain.interactor +import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.activity.data.repository.ActivityManagerRepository import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger @@ -30,8 +31,6 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map /** * Interactor representing a single notification's status bar chip. @@ -53,6 +52,7 @@ constructor( @StatusBarChipsLog private val logBuffer: LogBuffer, ) { private val key = startingModel.key + private val uid = startingModel.uid private val logger = Logger(logBuffer, "Notif".pad()) // [StatusBarChipLogTag] recommends a max tag length of 20, so [extraLogTag] should NOT be the // top-level tag. It should instead be provided as the first string in each log message. @@ -88,28 +88,36 @@ constructor( } return } + + if (model.uid != uid) { + logger.e({ + "$str1: received model with different uid, which shouldn't happen. " + + "Original UID: $int1, New UID: $int2. " + + "Proceeding as usual, but app visibility changes will be for *old* UID." + }) { + str1 = extraLogTag + int1 = uid + int2 = model.uid + } + } _notificationModel.value = model } - private val uid: Flow<Int> = _notificationModel.map { it.uid } - - /** True if the application managing the notification is visible to the user. */ - private val isAppVisible: Flow<Boolean> = - uid.flatMapLatest { currentUid -> - activityManagerRepository.createIsAppVisibleFlow(currentUid, logger, extraLogTag) - } + /** Details about when the app managing the notification was & is visible to the user. */ + private val appVisibility: Flow<AppVisibilityModel> = + activityManagerRepository.createAppVisibilityFlow(uid, logger, extraLogTag) /** * Emits this notification's status bar chip, or null if this notification shouldn't show a * status bar chip. */ val notificationChip: Flow<NotificationChipModel?> = - combine(_notificationModel, isAppVisible) { notif, isAppVisible -> - notif.toNotificationChipModel(isAppVisible) + combine(_notificationModel, appVisibility) { notif, appVisibility -> + notif.toNotificationChipModel(appVisibility) } private fun ActiveNotificationModel.toNotificationChipModel( - isVisible: Boolean + appVisibility: AppVisibilityModel ): NotificationChipModel? { val promotedContent = this.promotedContent if (promotedContent == null) { @@ -134,11 +142,13 @@ constructor( } return NotificationChipModel( - key, - appName, - statusBarChipIconView, - promotedContent, - isVisible, + key = key, + appName = appName, + statusBarChipIconView = statusBarChipIconView, + promotedContent = promotedContent, + creationTime = creationTime, + isAppVisible = appVisibility.isAppCurrentlyVisible, + lastAppVisibleTime = appVisibility.lastAppVisibleTime, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt index c26d10311f1e..edb44185459c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotif import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import javax.inject.Inject +import kotlin.math.max import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -39,9 +40,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** An interactor for the notification chips shown in the status bar. */ @SysUISingleton @@ -132,9 +135,6 @@ constructor( } interactor.setNotification(notif) } - logger.d({ "Interactors: $str1" }) { - str1 = promotedNotificationInteractorMap.keys.joinToString(separator = " /// ") - } promotedNotificationInteractors.value = promotedNotificationInteractorMap.values.toList() } @@ -145,26 +145,23 @@ constructor( * Emits all notifications that are eligible to show as chips in the status bar. This is * different from which chips will *actually* show, see [shownNotificationChips] for that. */ - private val allNotificationChips: Flow<List<NotificationChipModel>> = + val allNotificationChips: Flow<List<NotificationChipModel>> = if (StatusBarNotifChips.isEnabled) { // For all our current interactors... - promotedNotificationInteractors.flatMapLatest { intrs -> - // Stable-sort the promoted notifications by when they first appeared so that: - // 1) The chips don't switch places if the older chip gets a notification update. - // 2) The chips don't switch places when the second chip is tapped. (Whichever - // notification is showing heads-up is considered to be the top notification, which - // means tapping the second chip would move it to be the first chip if we didn't - // sort by appearance time here.) - // 3) Older chips get hidden if there's not enough room for all chips. - val interactors = intrs.sortedByDescending { it.creationTime } + // TODO(b/364653005): When a promoted notification is added or removed, each individual + // interactor's [notificationChip] flow becomes un-collected then re-collected, which + // can cause some flows to remove then add callbacks when they don't need to. Is there a + // better structure for this? Maybe Channels or a StateFlow with a short timeout? + promotedNotificationInteractors.flatMapLatest { interactors -> if (interactors.isNotEmpty()) { // Combine each interactor's [notificationChip] flow... val allNotificationChips: List<Flow<NotificationChipModel?>> = interactors.map { interactor -> interactor.notificationChip } combine(allNotificationChips) { - // ... and emit just the non-null chips - it.filterNotNull() - } + // ... and emit just the non-null & sorted chips + it.filterNotNull().sortedWith(chipComparator) + } + .logSort() } else { flowOf(emptyList()) } @@ -181,4 +178,35 @@ constructor( // out-of-sync (like a timer that's slightly off) chipsList.filter { !it.isAppVisible } } + + /* + Stable sort the promoted notifications by two criteria: + Criteria #1: Whichever app was most recently visible has higher ranking. + - Reasoning: If a user opened the app to see additional information, that's + likely the most important ongoing notification. + Criteria #2: Whichever notification first appeared more recently has higher ranking. + - Reasoning: Older chips get hidden if there's not enough room for all chips. + This semi-stable ordering ensures: + 1) The chips don't switch places if the older chip gets a notification update. + 2) The chips don't switch places when the second chip is tapped. (Whichever + notification is showing heads-up is considered to be the top notification, which + means tapping the second chip would move it to be the first chip if we didn't + sort by appearance time here.) + */ + private val chipComparator = + compareByDescending<NotificationChipModel> { + max(it.creationTime, it.lastAppVisibleTime ?: Long.MIN_VALUE) + } + + private fun Flow<List<NotificationChipModel>>.logSort(): Flow<List<NotificationChipModel>> { + return this.distinctUntilChanged().onEach { chips -> + val logString = + chips.joinToString { + "{key=${it.key}. " + + "lastVisibleAppTime=${it.lastAppVisibleTime}. " + + "creationTime=${it.creationTime}}" + } + logger.d({ "Sorted chips: $str1" }) { str1 = logString } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt index 97c37628f2e1..1f2079d83e6f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt @@ -26,6 +26,13 @@ data class NotificationChipModel( val appName: String, val statusBarChipIconView: StatusBarIconView?, val promotedContent: PromotedNotificationContentModel, + /** The time when the notification first appeared as promoted. */ + val creationTime: Long, /** True if the app managing this notification is currently visible to the user. */ val isAppVisible: Boolean, + /** + * The time when the app managing this notification last appeared as visible, or null if the app + * hasn't become visible since the notification became promoted. + */ + val lastAppVisibleTime: Long?, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 3ecbdf82f2cb..11e9fd56288f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -51,6 +52,7 @@ constructor( @Application private val applicationScope: CoroutineScope, private val notifChipsInteractor: StatusBarNotificationChipsInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, + private val systemClock: SystemClock, ) { /** * A flow modeling the notification chips that should be shown. Emits an empty list if there are @@ -158,16 +160,38 @@ constructor( clickBehavior, ) } + when (this.promotedContent.time.mode) { PromotedNotificationContentModel.When.Mode.BasicTime -> { - return OngoingActivityChipModel.Active.ShortTimeDelta( - this.key, - icon, - colors, - time = this.promotedContent.time.time, - onClickListenerLegacy, - clickBehavior, - ) + return if ( + this.promotedContent.time.time >= + systemClock.currentTimeMillis() + FUTURE_TIME_THRESHOLD_MILLIS + ) { + OngoingActivityChipModel.Active.ShortTimeDelta( + this.key, + icon, + colors, + time = this.promotedContent.time.time, + onClickListenerLegacy, + clickBehavior, + ) + } else { + // Don't show a `when` time that's close to now or in the past because it's + // likely that the app didn't intentionally set the `when` time to be shown in + // the status bar chip. + // TODO(b/393369213): If a notification sets a `when` time in the future and + // then that time comes and goes, the chip *will* start showing times in the + // past. Not going to fix this right now because the Compose implementation + // automatically handles this for us and we're hoping to launch the notification + // chips at the same time as the Compose chips. + return OngoingActivityChipModel.Active.IconOnly( + this.key, + icon, + colors, + onClickListenerLegacy, + clickBehavior, + ) + } } PromotedNotificationContentModel.When.Mode.CountUp -> { return OngoingActivityChipModel.Active.Timer( @@ -204,4 +228,12 @@ constructor( ) ) } + + companion object { + /** + * Notifications must have a `when` time of at least 1 minute in the future in order for the + * status bar chip to show the time. + */ + private const val FUTURE_TIME_THRESHOLD_MILLIS = 60 * 1000 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt index 2501aa59c375..5242feac898b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -37,7 +37,9 @@ import androidx.compose.ui.unit.constrain import androidx.compose.ui.unit.dp import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.formatTimeRemainingData import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerState +import com.android.systemui.statusbar.chips.ui.viewmodel.rememberTimeRemainingState import kotlin.math.min @Composable @@ -119,7 +121,26 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = } is OngoingActivityChipModel.Active.ShortTimeDelta -> { - // TODO(b/372657935): Implement ShortTimeDelta content in compose. + val timeRemainingState = rememberTimeRemainingState(futureTimeMillis = viewModel.time) + + timeRemainingState.timeRemainingData?.let { + val text = formatTimeRemainingData(it) + Text( + text = text, + style = textStyle, + color = textColor, + softWrap = false, + modifier = + modifier.hideTextIfDoesNotFit( + text = text, + textStyle = textStyle, + textMeasurer = textMeasurer, + maxTextWidth = maxTextWidth, + startPadding = startPadding, + endPadding = endPadding, + ), + ) + } } is OngoingActivityChipModel.Active.IconOnly -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt new file mode 100644 index 000000000000..eb6ebcaa5796 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025 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.ui.viewmodel + +import android.os.SystemClock +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlinx.coroutines.delay + +/** + * Manages state and updates for the duration remaining between now and a given time in the future. + */ +class TimeRemainingState(private val timeSource: TimeSource, private val futureTimeMillis: Long) { + private var durationRemaining by mutableStateOf(Duration.ZERO) + private var startTimeMillis: Long = 0 + + /** + * [Pair] representing the time unit and its value. + * + * @property first the string resource ID corresponding to the time unit (e.g., minutes, hours). + * @property second the time value of the duration unit. Null if time is less than a minute or + * past. + */ + val timeRemainingData by derivedStateOf { getTimeRemainingData(durationRemaining) } + + suspend fun run() { + startTimeMillis = timeSource.getCurrentTime() + while (true) { + val currentTime = timeSource.getCurrentTime() + durationRemaining = + (futureTimeMillis - currentTime).toDuration(DurationUnit.MILLISECONDS) + // No need to update if duration is more than 1 minute in the past. Because, we will + // stop displaying anything. + if (durationRemaining.inWholeMilliseconds < -1.minutes.inWholeMilliseconds) { + break + } + val delaySkewMillis = (currentTime - startTimeMillis) % 1000L + delay(calculateNextUpdateDelay(durationRemaining) - delaySkewMillis) + } + } + + private fun calculateNextUpdateDelay(duration: Duration): Long { + val durationAbsolute = duration.absoluteValue + return when { + durationAbsolute.inWholeHours < 1 -> { + 1000 + ((durationAbsolute.inWholeMilliseconds % 1.minutes.inWholeMilliseconds)) + } + durationAbsolute.inWholeHours < 24 -> { + 1000 + (durationAbsolute.inWholeMilliseconds % 1.hours.inWholeMilliseconds) + } + else -> 1000 + (durationAbsolute.inWholeMilliseconds % 24.hours.inWholeMilliseconds) + } + } +} + +/** Remember and manage the TimeRemainingState */ +@Composable +fun rememberTimeRemainingState( + futureTimeMillis: Long, + timeSource: TimeSource = remember { TimeSource { SystemClock.elapsedRealtime() } }, +): TimeRemainingState { + + val state = + remember(timeSource, futureTimeMillis) { TimeRemainingState(timeSource, futureTimeMillis) } + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner, timeSource, futureTimeMillis) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { state.run() } + } + + return state +} + +private fun getTimeRemainingData(duration: Duration): Pair<Int, Long?>? { + return when { + duration.inWholeMinutes <= -1 -> null + duration.inWholeMinutes < 1 -> Pair(com.android.internal.R.string.now_string_shortest, null) + duration.inWholeHours < 1 -> + Pair(com.android.internal.R.string.duration_minutes_medium, duration.inWholeMinutes) + duration.inWholeDays < 1 -> + Pair(com.android.internal.R.string.duration_hours_medium, duration.inWholeHours) + else -> null + } +} + +/** Formats the time remaining data into a user-readable string. */ +@Composable +fun formatTimeRemainingData(resourcePair: Pair<Int, Long?>): String { + return resourcePair.let { (resourceId, time) -> + when (time) { + null -> stringResource(resourceId) + else -> stringResource(resourceId, time.toInt()) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt new file mode 100644 index 000000000000..e7bc052114eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/MediaControlChipStartable.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 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.featurepods.media + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * A [CoreStartable] that initializes and starts the media control chip functionality. The media + * chip is limited to large screen devices currently. Therefore, this [CoreStartable] should not be + * used for phones or smaller form factor devices. + */ +@SysUISingleton +class MediaControlChipStartable +@Inject +constructor( + @Background val bgScope: CoroutineScope, + private val mediaControlChipInteractor: MediaControlChipInteractor, +) : CoreStartable { + + override fun start() { + bgScope.launch { mediaControlChipInteractor.initialize() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt index e3e77e16be6d..f439bb297de0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt @@ -22,13 +22,15 @@ import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaCommonModel import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** @@ -37,6 +39,8 @@ import kotlinx.coroutines.flow.stateIn * Provides a [StateFlow] of [MediaControlChipModel] representing the current state of the media * control chip. Emits a new [MediaControlChipModel] when there is an active media session and the * corresponding user preference is found, otherwise emits null. + * + * This functionality is only enabled on large screen devices. */ @SysUISingleton class MediaControlChipInteractor @@ -45,30 +49,57 @@ constructor( @Background private val backgroundScope: CoroutineScope, mediaFilterRepository: MediaFilterRepository, ) { - private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> = - mediaFilterRepository.currentMedia - .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() } - .stateIn( - scope = backgroundScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList(), - ) + private val isEnabled = MutableStateFlow(false) + + private val mediaControlChipModelForScene: Flow<MediaControlChipModel?> = + combine(mediaFilterRepository.currentMedia, mediaFilterRepository.selectedUserEntries) { + mediaList, + userEntries -> + mediaList + .filterIsInstance<MediaCommonModel.MediaControl>() + .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] } + .firstOrNull { it.active } + ?.toMediaControlChipModel() + } + + /** + * A flow of [MediaControlChipModel] representing the current state of the media controls chip. + * This flow emits null when no active media is playing or when playback information is + * unavailable. This flow is only active when [SceneContainerFlag] is disabled. + */ + private val mediaControlChipModelLegacy = MutableStateFlow<MediaControlChipModel?>(null) + + fun updateMediaControlChipModelLegacy(mediaData: MediaData?) { + if (!SceneContainerFlag.isEnabled) { + mediaControlChipModelLegacy.value = mediaData?.toMediaControlChipModel() + } + } + + private val _mediaControlChipModel: Flow<MediaControlChipModel?> = + if (SceneContainerFlag.isEnabled) { + mediaControlChipModelForScene + } else { + mediaControlChipModelLegacy + } /** The currently active [MediaControlChipModel] */ - val mediaControlModel: StateFlow<MediaControlChipModel?> = - combine(currentMediaControls, mediaFilterRepository.selectedUserEntries) { - mediaControls, - userEntries -> - mediaControls - .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] } - .firstOrNull { it.active } - ?.toMediaControlChipModel() + val mediaControlChipModel: StateFlow<MediaControlChipModel?> = + combine(_mediaControlChipModel, isEnabled) { mediaControlChipModel, isEnabled -> + if (isEnabled) { + mediaControlChipModel + } else { + null + } } - .stateIn( - scope = backgroundScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) + .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), null) + + /** + * The media control chip may not be enabled on all form factors, so only the relevant form + * factors should initialize the interactor. This must be called from a CoreStartable. + */ + fun initialize() { + isEnabled.value = true + } } private fun MediaData.toMediaControlChipModel(): MediaControlChipModel { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt index 19acb2e9839c..7f0f6078f391 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt @@ -55,8 +55,8 @@ constructor( * whenever the underlying [MediaControlChipModel] changes. */ override val chip: StateFlow<PopupChipModel> = - mediaControlChipInteractor.mediaControlModel - .map { mediaControlModel -> toPopupChipModel(mediaControlModel) } + mediaControlChipInteractor.mediaControlChipModel + .map { mediaControlChipModel -> toPopupChipModel(mediaControlChipModel) } .stateIn( backgroundScope, SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java index 37485feed792..0e3f103c152e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java @@ -16,11 +16,74 @@ package com.android.systemui.statusbar.notification.collection; +import static android.app.NotificationChannel.NEWS_ID; +import static android.app.NotificationChannel.PROMOTIONS_ID; +import static android.app.NotificationChannel.RECS_ID; +import static android.app.NotificationChannel.SOCIAL_MEDIA_ID; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; + +import java.util.List; + /** * Abstract class to represent notification section bundled by AI. */ public class BundleEntry extends PipelineEntry { + private final String mKey; + private final BundleEntryAdapter mEntryAdapter; + + // TODO (b/389839319): implement the row + private ExpandableNotificationRow mRow; + + public BundleEntry(String key) { + mKey = key; + mEntryAdapter = new BundleEntryAdapter(); + } + + @VisibleForTesting + public BundleEntryAdapter getEntryAdapter() { + return mEntryAdapter; + } + public class BundleEntryAdapter implements EntryAdapter { + + /** + * TODO (b/394483200): convert to PipelineEntry.ROOT_ENTRY when pipeline is migrated? + */ + @Override + public GroupEntry getParent() { + return GroupEntry.ROOT_ENTRY; + } + + @Override + public boolean isTopLevelEntry() { + return true; + } + + @Override + public String getKey() { + return mKey; + } + + @Override + public ExpandableNotificationRow getRow() { + return mRow; + } + + @Nullable + @Override + public EntryAdapter getGroupRoot() { + return this; + } } + + public static final List<BundleEntry> ROOT_BUNDLES = List.of( + new BundleEntry(PROMOTIONS_ID), + new BundleEntry(SOCIAL_MEDIA_ID), + new BundleEntry(NEWS_ID), + new BundleEntry(RECS_ID)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java index b12b1c538a32..4df81c97e21e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java @@ -16,8 +16,52 @@ package com.android.systemui.statusbar.notification.collection; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; + /** * Adapter interface for UI to get relevant info. */ public interface EntryAdapter { + + /** + * Gets the parent of this entry, or null if the entry's view is not attached + */ + @Nullable PipelineEntry getParent(); + + /** + * Returns whether the entry is attached and appears at the top level of the shade + */ + boolean isTopLevelEntry(); + + /** + * @return the unique identifier for this entry + */ + @NonNull String getKey(); + + /** + * Gets the view that this entry is backing. + */ + @NonNull + ExpandableNotificationRow getRow(); + + /** + * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry + * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the + * BundleEntry will be returned. If the given notification is a group summary NotificationEntry, + * or a child of a group summary, the summary NotificationEntry will be returned, even if that + * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any + * group or bundle grouping, null will be returned. + */ + @Nullable + EntryAdapter getGroupRoot(); + + /** + * Returns whether the entry is attached to the current shade list + */ + default boolean isAttached() { + return getParent() != null; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java index fc47dc1ed81a..8f3c357a277a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.NotifInf import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import javax.inject.Inject; @@ -78,7 +79,7 @@ public class NotifInflaterImpl implements NotifInflater { requireBinder().inflateViews( entry, params, - wrapInflationCallback(callback)); + wrapInflationCallback(entry, callback)); } catch (InflationException e) { mLogger.logInflationException(entry, e); mNotifErrorManager.setInflationError(entry, e); @@ -101,17 +102,26 @@ public class NotifInflaterImpl implements NotifInflater { } private NotificationRowContentBinder.InflationCallback wrapInflationCallback( + final NotificationEntry entry, InflationCallback callback) { return new NotificationRowContentBinder.InflationCallback() { @Override public void handleInflationException( NotificationEntry entry, Exception e) { + if (NotificationBundleUi.isEnabled()) { + handleInflationException(e); + } else { + mNotifErrorManager.setInflationError(entry, e); + } + } + @Override + public void handleInflationException(Exception e) { mNotifErrorManager.setInflationError(entry, e); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { mNotifErrorManager.clearInflationError(entry); if (callback != null) { callback.onInflationFinished(entry, entry.getRowController()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 7dd82a6b5198..90f9525c7683 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -29,6 +29,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; +import static com.android.systemui.statusbar.notification.collection.BundleEntry.ROOT_BUNDLES; +import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING; @@ -107,6 +109,7 @@ public final class NotificationEntry extends ListEntry { private final String mKey; private StatusBarNotification mSbn; private Ranking mRanking; + private final NotifEntryAdapter mEntryAdapter; /* * Bookkeeping members @@ -268,9 +271,48 @@ public final class NotificationEntry extends ListEntry { mKey = sbn.getKey(); setSbn(sbn); setRanking(ranking); + mEntryAdapter = new NotifEntryAdapter(); } public class NotifEntryAdapter implements EntryAdapter { + @Override + public PipelineEntry getParent() { + return NotificationEntry.this.getParent(); + } + + @Override + public boolean isTopLevelEntry() { + return getParent() != null + && (getParent() == ROOT_ENTRY || ROOT_BUNDLES.contains(getParent())); + } + + @Override + public String getKey() { + return NotificationEntry.this.getKey(); + } + + @Override + public ExpandableNotificationRow getRow() { + return NotificationEntry.this.getRow(); + } + + @Nullable + @Override + public EntryAdapter getGroupRoot() { + // TODO (b/395857098): for backwards compatibility this will return null if called + // on a group summary that's not in a bundles, but it should return itself. + if (isTopLevelEntry() || getParent() == null) { + return null; + } + if (NotificationEntry.this.getParent().getSummary() != null) { + return NotificationEntry.this.getParent().getSummary().mEntryAdapter; + } + return null; + } + } + + public EntryAdapter getEntryAdapter() { + return mEntryAdapter; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java index efedfef5cbe9..c5a479180329 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java @@ -19,5 +19,5 @@ package com.android.systemui.statusbar.notification.collection; /** * Class to represent a notification, group, or bundle in the pipeline. */ -public class PipelineEntry { +public abstract class PipelineEntry { } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java index 733b986b5422..9df4bf4af4e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinator.java @@ -23,6 +23,7 @@ import android.app.Notification; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -31,29 +32,50 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; +import com.android.systemui.statusbar.notification.promoted.domain.interactor.PromotedNotificationsInteractor; import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; +import com.android.systemui.util.kotlin.JavaAdapterKt; -import com.google.common.primitives.Booleans; +import kotlinx.coroutines.CoroutineScope; + +import java.util.Collections; +import java.util.List; import javax.inject.Inject; /** * Handles sectioning for foreground service notifications. - * Puts non-min colorized foreground service notifications into the FGS section. See - * {@link NotifCoordinators} for section ordering priority. + * Puts non-min colorized foreground service notifications into the FGS section. See + * {@link NotifCoordinators} for section ordering priority. */ @CoordinatorScope public class ColorizedFgsCoordinator implements Coordinator { private static final String TAG = "ColorizedCoordinator"; + private final PromotedNotificationsInteractor mPromotedNotificationsInteractor; + private final CoroutineScope mMainScope; + + private List<String> mOrderedPromotedNotifKeys = Collections.emptyList(); @Inject - public ColorizedFgsCoordinator() { + public ColorizedFgsCoordinator( + @Application CoroutineScope mainScope, + PromotedNotificationsInteractor promotedNotificationsInteractor + ) { + mPromotedNotificationsInteractor = promotedNotificationsInteractor; + mMainScope = mainScope; } @Override - public void attach(NotifPipeline pipeline) { + public void attach(@NonNull NotifPipeline pipeline) { if (PromotedNotificationUi.isEnabled()) { pipeline.addPromoter(mPromotedOngoingPromoter); + + JavaAdapterKt.collectFlow(mMainScope, + mPromotedNotificationsInteractor.getOrderedChipNotificationKeys(), + (List<String> keys) -> { + mOrderedPromotedNotifKeys = keys; + mNotifSectioner.invalidateList("updated mOrderedPromotedNotifKeys"); + }); } } @@ -82,12 +104,24 @@ public class ColorizedFgsCoordinator implements Coordinator { return false; } - private NotifComparator mPreferPromoted = new NotifComparator("PreferPromoted") { + /** get the sort key for any entry in the ongoing section */ + private int getSortKey(@Nullable NotificationEntry entry) { + if (entry == null) return Integer.MAX_VALUE; + // Order all promoted notif keys first, using their order in the list + final int index = mOrderedPromotedNotifKeys.indexOf(entry.getKey()); + if (index >= 0) return index; + // Next, prioritize promoted ongoing over other notifications + return isPromotedOngoing(entry) ? Integer.MAX_VALUE - 1 : Integer.MAX_VALUE; + } + + private final NotifComparator mOngoingComparator = new NotifComparator( + "OngoingComparator") { @Override public int compare(@NonNull ListEntry o1, @NonNull ListEntry o2) { - return -1 * Booleans.compare( - isPromotedOngoing(o1.getRepresentativeEntry()), - isPromotedOngoing(o2.getRepresentativeEntry())); + return Integer.compare( + getSortKey(o1.getRepresentativeEntry()), + getSortKey(o2.getRepresentativeEntry()) + ); } }; @@ -95,7 +129,7 @@ public class ColorizedFgsCoordinator implements Coordinator { @Override public NotifComparator getComparator() { if (PromotedNotificationUi.isEnabled()) { - return mPreferPromoted; + return mOngoingComparator; } else { return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java index d47fe20911f9..2e3ab926ad57 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/HighPriorityProvider.java @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.List; @@ -129,21 +130,42 @@ public class HighPriorityProvider { >= NotificationManager.IMPORTANCE_DEFAULT); } + /** + * Returns whether the given ListEntry has a high priority child or is in a group with a child + * that's high priority + */ private boolean hasHighPriorityChild(ListEntry entry, boolean allowImplicit) { - if (entry instanceof NotificationEntry - && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) { - return false; - } - - List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry); - if (children != null) { - for (NotificationEntry child : children) { - if (child != entry && isHighPriority(child, allowImplicit)) { - return true; + if (NotificationBundleUi.isEnabled()) { + GroupEntry representativeGroupEntry = null; + if (entry instanceof GroupEntry) { + representativeGroupEntry = (GroupEntry) entry; + } else if (entry instanceof NotificationEntry){ + final NotificationEntry notificationEntry = entry.getRepresentativeEntry(); + if (notificationEntry.getParent() != null + && notificationEntry.getParent().getSummary() != null + && notificationEntry.getParent().getSummary() == notificationEntry) { + representativeGroupEntry = notificationEntry.getParent(); } } + return representativeGroupEntry != null && + representativeGroupEntry.getChildren().stream().anyMatch( + childEntry -> isHighPriority(childEntry, allowImplicit)); + + } else { + if (entry instanceof NotificationEntry + && !mGroupMembershipManager.isGroupSummary((NotificationEntry) entry)) { + return false; + } + List<NotificationEntry> children = mGroupMembershipManager.getChildren(entry); + if (children != null) { + for (NotificationEntry child : children) { + if (child != entry && isHighPriority(child, allowImplicit)) { + return true; + } + } + } + return false; } - return false; } private boolean hasHighPriorityCharacteristics(NotificationEntry entry) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java index 30386ab46382..ea369463da51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManager.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.collection.render; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -38,6 +39,20 @@ public interface GroupExpansionManager { boolean isGroupExpanded(NotificationEntry entry); /** + * Whether the parent associated with this notification is expanded. + * If this notification is not part of a group or bundle, it will always return false. + */ + boolean isGroupExpanded(EntryAdapter entry); + + /** + * Set whether the group/bundle associated with this notification is expanded or not. + */ + void setGroupExpanded(EntryAdapter entry, boolean expanded); + + /** @return group/bundle expansion state after toggling. */ + boolean toggleGroupExpansion(EntryAdapter entry); + + /** * Set whether the group associated with this notification is expanded or not. */ void setGroupExpanded(NotificationEntry entry, boolean expanded); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java index d1aff80b4e7c..16b98e20498a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java @@ -23,11 +23,13 @@ import androidx.annotation.NonNull; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.io.PrintWriter; import java.util.ArrayList; @@ -55,6 +57,8 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl */ private final Set<NotificationEntry> mExpandedGroups = new HashSet<>(); + private final Set<EntryAdapter> mExpandedCollections = new HashSet<>(); + @Inject public GroupExpansionManagerImpl(DumpManager dumpManager, GroupMembershipManager groupMembershipManager) { @@ -63,11 +67,17 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } /** - * Cleanup entries from mExpandedGroups that no longer exist in the pipeline. + * Cleanup entries from internal tracking that no longer exist in the pipeline. */ private final OnBeforeRenderListListener mNotifTracker = (entries) -> { - if (mExpandedGroups.isEmpty()) { - return; // nothing to do + if (NotificationBundleUi.isEnabled()) { + if (mExpandedCollections.isEmpty()) { + return; // nothing to do + } + } else { + if (mExpandedGroups.isEmpty()) { + return; // nothing to do + } } final Set<NotificationEntry> renderingSummaries = new HashSet<>(); @@ -77,10 +87,25 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } } - // If a group is in mExpandedGroups but not in the pipeline entries, collapse it. - final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries); - for (NotificationEntry entry : groupsToRemove) { - setGroupExpanded(entry, false); + if (NotificationBundleUi.isEnabled()) { + for (EntryAdapter entryAdapter : mExpandedCollections) { + boolean isInPipeline = false; + for (NotificationEntry entry : renderingSummaries) { + if (entry.getKey().equals(entryAdapter.getKey())) { + isInPipeline = true; + break; + } + } + if (!isInPipeline) { + setGroupExpanded(entryAdapter, false); + } + } + } else { + // If a group is in mExpandedGroups but not in the pipeline entries, collapse it. + final var groupsToRemove = setDifference(mExpandedGroups, renderingSummaries); + for (NotificationEntry entry : groupsToRemove) { + setGroupExpanded(entry, false); + } } }; @@ -96,11 +121,13 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public boolean isGroupExpanded(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); return mExpandedGroups.contains(mGroupMembershipManager.getGroupSummary(entry)); } @Override public void setGroupExpanded(NotificationEntry entry, boolean expanded) { + NotificationBundleUi.assertInLegacyMode(); NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry); if (entry.getParent() == null) { if (expanded) { @@ -127,14 +154,61 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public boolean toggleGroupExpansion(NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); + setGroupExpanded(entry, !isGroupExpanded(entry)); + return isGroupExpanded(entry); + } + + @Override + public boolean isGroupExpanded(EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return mExpandedCollections.contains(mGroupMembershipManager.getGroupRoot(entry)); + } + + @Override + public void setGroupExpanded(EntryAdapter entry, boolean expanded) { + NotificationBundleUi.assertInNewMode(); + EntryAdapter groupParent = mGroupMembershipManager.getGroupRoot(entry); + if (!entry.isAttached()) { + if (expanded) { + Log.wtf(TAG, "Cannot expand group that is not attached"); + } else { + // The entry is no longer attached, but we still want to make sure we don't have + // a stale expansion state. + groupParent = entry; + } + } + + boolean changed; + if (expanded) { + changed = mExpandedCollections.add(groupParent); + } else { + changed = mExpandedCollections.remove(groupParent); + } + + // Only notify listeners if something changed. + if (changed) { + sendOnGroupExpandedChange(entry, expanded); + } + } + + @Override + public boolean toggleGroupExpansion(EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); setGroupExpanded(entry, !isGroupExpanded(entry)); return isGroupExpanded(entry); } @Override public void collapseGroups() { - for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) { - setGroupExpanded(entry, false); + if (NotificationBundleUi.isEnabled()) { + for (EntryAdapter entry : new ArrayList<>(mExpandedCollections)) { + setGroupExpanded(entry, false); + } + } else { + for (NotificationEntry entry : new ArrayList<>(mExpandedGroups)) { + setGroupExpanded(entry, false); + } } } @@ -145,9 +219,21 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl for (NotificationEntry entry : mExpandedGroups) { pw.println(" * " + entry.getKey()); } + pw.println(" mExpandedCollection: " + mExpandedCollections.size()); + for (EntryAdapter entry : mExpandedCollections) { + pw.println(" * " + entry.getKey()); + } } private void sendOnGroupExpandedChange(NotificationEntry entry, boolean expanded) { + NotificationBundleUi.assertInLegacyMode(); + for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) { + listener.onGroupExpansionChange(entry.getRow(), expanded); + } + } + + private void sendOnGroupExpandedChange(EntryAdapter entry, boolean expanded) { + NotificationBundleUi.assertInNewMode(); for (OnGroupExpansionChangeListener listener : mOnGroupChangeListeners) { listener.onGroupExpansionChange(entry.getRow(), expanded); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java index 3158782e6fea..69267e5d9e55 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.collection.render; import android.annotation.NonNull; import android.annotation.Nullable; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -29,6 +30,13 @@ import java.util.List; * generally assumes that the notification is attached (aka its parent is not null). */ public interface GroupMembershipManager { + + /** + * @return whether a given entry is the root (GroupEntry or BundleEntry) in a collection which + * has children + */ + boolean isGroupRoot(@NonNull EntryAdapter entry); + /** * @return whether a given notification is the summary in a group which has children */ @@ -42,16 +50,15 @@ public interface GroupMembershipManager { NotificationEntry getGroupSummary(@NonNull NotificationEntry entry); /** - * Similar to {@link #getGroupSummary(NotificationEntry)} but doesn't get the visual summary - * but the logical summary, i.e when a child is isolated, it still returns the summary as if - * it wasn't isolated. - * TODO: remove this when migrating to the new pipeline, this is taken care of in the - * dismissal logic built into NotifCollection + * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry + * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the + * BundleEntry will be returned. If the given notification is a group summary NotificationEntry, + * or a child of a group summary, the summary NotificationEntry will be returned, even if that + * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any + * group or bundle grouping, null will be returned. */ @Nullable - default NotificationEntry getLogicalGroupSummary(@NonNull NotificationEntry entry) { - return getGroupSummary(entry); - } + EntryAdapter getGroupRoot(@NonNull EntryAdapter entry); /** * @return whether a given notification is a child in a group @@ -59,9 +66,10 @@ public interface GroupMembershipManager { boolean isChildInGroup(@NonNull NotificationEntry entry); /** - * Whether this is the only child in a group + * @return whether a given notification is a child in a group. The group may be a notification + * group or a bundle. */ - boolean isOnlyChildInGroup(@NonNull NotificationEntry entry); + boolean isChildInGroup(@NonNull EntryAdapter entry); /** * Get the children that are in the summary's group, not including those isolated. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java index da1247953c4c..80a9f8adf8f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java @@ -22,9 +22,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import java.util.List; @@ -41,6 +43,7 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { @Override public boolean isGroupSummary(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry.getParent() == null) { // The entry is not attached, so it doesn't count. return false; @@ -49,33 +52,47 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { return entry.getParent().getSummary() == entry; } + @Override + public boolean isGroupRoot(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return entry == entry.getGroupRoot(); + } + @Nullable @Override public NotificationEntry getGroupSummary(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (isTopLevelEntry(entry) || entry.getParent() == null) { return null; } return entry.getParent().getSummary(); } + @Nullable + @Override + public EntryAdapter getGroupRoot(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + return entry.getGroupRoot(); + } + @Override public boolean isChildInGroup(@NonNull NotificationEntry entry) { + NotificationBundleUi.assertInLegacyMode(); // An entry is a child if it's not a summary or top level entry, but it is attached. return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null; } @Override - public boolean isOnlyChildInGroup(@NonNull NotificationEntry entry) { - if (entry.getParent() == null) { - return false; // The entry is not attached. - } - - return !isGroupSummary(entry) && entry.getParent().getChildren().size() == 1; + public boolean isChildInGroup(@NonNull EntryAdapter entry) { + NotificationBundleUi.assertInNewMode(); + // An entry is a child if it's not a group root or top level entry, but it is attached. + return entry.isAttached() && entry != getGroupRoot(entry) && !entry.isTopLevelEntry(); } @Nullable @Override public List<NotificationEntry> getChildren(@NonNull ListEntry entry) { + NotificationBundleUi.assertInLegacyMode(); if (entry instanceof GroupEntry) { return ((GroupEntry) entry).getChildren(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt deleted file mode 100644 index bcaf1878a869..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStackOptionalModule.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.dagger - -import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHider -import com.android.systemui.statusbar.notification.stack.NotificationStackRebindingHiderImpl -import dagger.Binds -import dagger.BindsOptionalOf -import dagger.Module - -/** - * This is meant to be bound in SystemUI variants with [NotificationStackScrollLayoutController]. - */ -@Module -interface NotificationStackModule { - @Binds - fun bindNotificationStackRebindingHider( - impl: NotificationStackRebindingHiderImpl - ): NotificationStackRebindingHider -} - -/** This is meant to be used by all SystemUI variants, also those without NSSL. */ -@Module -interface NotificationStackOptionalModule { - @BindsOptionalOf - fun bindOptionalOfNotificationStackRebindingHider(): NotificationStackRebindingHider -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 34f4969127e3..53d5dbc58677 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -121,7 +121,6 @@ import javax.inject.Provider; NotificationMemoryModule.class, NotificationStatsLoggerModule.class, NotificationsLogModule.class, - NotificationStackOptionalModule.class, }) public interface NotificationsModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index be61dc95fe20..7d74a496853f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -46,6 +46,7 @@ import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; @@ -55,6 +56,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -118,7 +120,8 @@ public class HeadsUpManagerImpl @VisibleForTesting final ArrayMap<String, HeadsUpEntry> mHeadsUpEntryMap = new ArrayMap<>(); private final HeadsUpManagerLogger mLogger; - private final int mMinimumDisplayTime; + private final int mMinimumDisplayTimeDefault; + private final int mMinimumDisplayTimeForUserInitiated; private final int mStickyForSomeTimeAutoDismissTime; private final int mAutoDismissTime; private final DelayableExecutor mExecutor; @@ -215,9 +218,11 @@ public class HeadsUpManagerImpl mGroupMembershipManager = groupMembershipManager; mVisualStabilityProvider = visualStabilityProvider; Resources resources = context.getResources(); - mMinimumDisplayTime = NotificationThrottleHun.isEnabled() + mMinimumDisplayTimeDefault = NotificationThrottleHun.isEnabled() ? resources.getInteger(R.integer.heads_up_notification_minimum_time_with_throttling) : resources.getInteger(R.integer.heads_up_notification_minimum_time); + mMinimumDisplayTimeForUserInitiated = resources.getInteger( + R.integer.heads_up_notification_minimum_time_for_user_initiated); mStickyForSomeTimeAutoDismissTime = resources.getInteger( R.integer.sticky_heads_up_notification_time); mAutoDismissTime = resources.getInteger(R.integer.heads_up_notification_decay); @@ -871,14 +876,24 @@ public class HeadsUpManagerImpl if (!hasPinnedHeadsUp() || topEntry == null) { return null; } else { + ExpandableNotificationRow topRow = topEntry.getRow(); if (topEntry.rowIsChildInGroup()) { - final NotificationEntry groupSummary = - mGroupMembershipManager.getGroupSummary(topEntry); - if (groupSummary != null) { - topEntry = groupSummary; + if (NotificationBundleUi.isEnabled()) { + final EntryAdapter adapter = mGroupMembershipManager.getGroupRoot( + topRow.getEntryAdapter()); + if (adapter != null) { + topRow = adapter.getRow(); + } + } else { + final NotificationEntry groupSummary = + mGroupMembershipManager.getGroupSummary(topEntry); + if (groupSummary != null) { + topEntry = groupSummary; + topRow = topEntry.getRow(); + } } } - ExpandableNotificationRow topRow = topEntry.getRow(); + int[] tmpArray = new int[2]; topRow.getLocationOnScreen(tmpArray); int minX = tmpArray[0]; @@ -1358,7 +1373,12 @@ public class HeadsUpManagerImpl final long now = mSystemClock.elapsedRealtime(); if (updateEarliestRemovalTime) { - mEarliestRemovalTime = now + mMinimumDisplayTime; + if (StatusBarNotifChips.isEnabled() + && mPinnedStatus.getValue() == PinnedStatus.PinnedByUser) { + mEarliestRemovalTime = now + mMinimumDisplayTimeForUserInitiated; + } else { + mEarliestRemovalTime = now + mMinimumDisplayTimeDefault; + } } if (updatePostTime) { @@ -1377,7 +1397,7 @@ public class HeadsUpManagerImpl final long now = mSystemClock.elapsedRealtime(); return NotificationThrottleHun.isEnabled() ? Math.max(finishTime, mEarliestRemovalTime) - now - : Math.max(finishTime - now, mMinimumDisplayTime); + : Math.max(finishTime - now, mMinimumDisplayTimeDefault); }; scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt index f02edee399eb..18a1afa17720 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.statusbar.data.repository.NotificationListenerSettin import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor +import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.wm.shell.bubbles.Bubbles import java.util.Optional @@ -30,6 +31,7 @@ import kotlin.jvm.optionals.getOrNull import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn /** Domain logic related to notification icons. */ @@ -39,8 +41,21 @@ constructor( private val activeNotificationsInteractor: ActiveNotificationsInteractor, private val bubbles: Optional<Bubbles>, private val headsUpNotificationIconInteractor: HeadsUpNotificationIconInteractor, + private val aodPromotedNotificationInteractor: AODPromotedNotificationInteractor, private val keyguardViewStateRepository: NotificationsKeyguardViewStateRepository, ) { + private val aodPromotedKeyToHide: Flow<String?> = + combine( + aodPromotedNotificationInteractor.content, + aodPromotedNotificationInteractor.isPresent, + ) { content, isPresent -> + when { + !isPresent -> null + content == null -> null + else -> content.identity.key + } + } + /** Returns a subset of all active notifications based on the supplied filtration parameters. */ fun filteredNotifSet( forceShowHeadsUp: Boolean = false, @@ -49,12 +64,14 @@ constructor( showDismissed: Boolean = true, showRepliedMessages: Boolean = true, showPulsing: Boolean = true, + showAodPromoted: Boolean = true, ): Flow<Set<ActiveNotificationModel>> { return combine( activeNotificationsInteractor.topLevelRepresentativeNotifications, headsUpNotificationIconInteractor.isolatedNotification, + if (showAodPromoted) flowOf(null) else aodPromotedKeyToHide, keyguardViewStateRepository.areNotificationsFullyHidden, - ) { notifications, isolatedNotifKey, notifsFullyHidden -> + ) { notifications, isolatedNotifKey, aodPromotedKeyToHide, notifsFullyHidden -> notifications .asSequence() .filter { model: ActiveNotificationModel -> @@ -67,6 +84,7 @@ constructor( showRepliedMessages = showRepliedMessages, showPulsing = showPulsing, isolatedNotifKey = isolatedNotifKey, + aodPromotedKeyToHide = aodPromotedKeyToHide, notifsFullyHidden = notifsFullyHidden, ) } @@ -83,6 +101,7 @@ constructor( showRepliedMessages: Boolean, showPulsing: Boolean, isolatedNotifKey: String?, + aodPromotedKeyToHide: String?, notifsFullyHidden: Boolean, ): Boolean { return when { @@ -93,6 +112,7 @@ constructor( !showRepliedMessages && model.isLastMessageFromReply -> false !showAmbient && model.isSuppressedFromStatusBar -> false !showPulsing && model.isPulsing && !notifsFullyHidden -> false + model.key == aodPromotedKeyToHide -> false bubbles.getOrNull()?.isBubbleExpanded(model.key) == true -> false else -> true } @@ -115,6 +135,7 @@ constructor( showDismissed = false, showRepliedMessages = false, showPulsing = !isBypassEnabled, + showAodPromoted = false, ) } .flowOn(bgContext) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt index 691f1f452da8..f755dbb48e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleNotificationIdentifier.kt @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.notification.people.PeopleNotificationIden import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_IMPORTANT_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import javax.inject.Inject import kotlin.math.max @@ -112,14 +113,26 @@ class PeopleNotificationIdentifierImpl @Inject constructor( if (personExtractor.isPersonNotification(sbn)) TYPE_PERSON else TYPE_NON_PERSON private fun getPeopleTypeOfSummary(entry: NotificationEntry): Int { - if (!groupManager.isGroupSummary(entry)) { - return TYPE_NON_PERSON + if (NotificationBundleUi.isEnabled) { + if (!entry.sbn.notification.isGroupSummary) { + return TYPE_NON_PERSON; + } + + return getPeopleTypeForChildList(entry.parent?.children) + } else { + if (!groupManager.isGroupSummary(entry)) { + return TYPE_NON_PERSON + } + + return getPeopleTypeForChildList(groupManager.getChildren(entry)) } + } - val childTypes = groupManager.getChildren(entry) - ?.asSequence() - ?.map { getPeopleNotificationType(it) } - ?: return TYPE_NON_PERSON + private fun getPeopleTypeForChildList(children: List<NotificationEntry>?): Int { + val childTypes = children + ?.asSequence() + ?.map { getPeopleNotificationType(it) } + ?: return TYPE_NON_PERSON var groupType = TYPE_NON_PERSON for (childType in childTypes) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index e5d2361e8524..893570b7fb51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt @@ -426,6 +426,8 @@ private class AODPromotedNotificationViewUpdater(root: View) { chronometer = chronometerStub?.inflate() as Chronometer chronometerStub = null + + chronometer?.appendFontFeatureSetting("tnum") } private fun inflateOldProgressBar() { @@ -501,6 +503,10 @@ private fun Notification.ProgressStyle.Point.toSkeleton(): Notification.Progress } } +private fun TextView.appendFontFeatureSetting(newSetting: String) { + fontFeatureSettings = (fontFeatureSettings?.let { "$it," } ?: "") + newSetting +} + private enum class AodPromotedNotificationColor(val colorInt: Int) { Background(android.graphics.Color.BLACK), PrimaryText(android.graphics.Color.WHITE), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt index 393f95d3ad77..4bc685423659 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style import com.android.systemui.util.kotlin.FlowDumperImpl @@ -30,13 +29,12 @@ import kotlinx.coroutines.flow.map class AODPromotedNotificationInteractor @Inject constructor( - activeNotificationsInteractor: ActiveNotificationsInteractor, + promotedNotificationsInteractor: PromotedNotificationsInteractor, dumpManager: DumpManager, ) : FlowDumperImpl(dumpManager) { + /** The content to show as the promoted notification on AOD */ val content: Flow<PromotedNotificationContentModel?> = - activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs -> - notifs.firstNotNullOfOrNull { it.promotedContent } - } + promotedNotificationsInteractor.topPromotedNotificationContent val isPresent: Flow<Boolean> = content diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt new file mode 100644 index 000000000000..1015cfbefc41 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractor.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor +import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * An interactor that provides details about promoted notification precedence, based on the + * presented order of current notification status bar chips. + */ +@SysUISingleton +class PromotedNotificationsInteractor +@Inject +constructor( + activeNotificationsInteractor: ActiveNotificationsInteractor, + callChipInteractor: CallChipInteractor, + notifChipsInteractor: StatusBarNotificationChipsInteractor, + @Background backgroundDispatcher: CoroutineDispatcher, +) { + /** + * This is the ordered list of notifications (and the promoted content) represented as chips in + * the status bar. + */ + private val orderedChipNotifications: Flow<List<NotifAndPromotedContent>> = + combine(callChipInteractor.ongoingCallState, notifChipsInteractor.allNotificationChips) { + callState, + notifChips -> + buildList { + val callData = callState.getNotifData()?.also { add(it) } + addAll( + notifChips.mapNotNull { + when (it.key) { + callData?.key -> null // do not re-add the same call + else -> NotifAndPromotedContent(it.key, it.promotedContent) + } + } + ) + } + } + + private fun OngoingCallModel.getNotifData(): NotifAndPromotedContent? = + when (this) { + is OngoingCallModel.InCall -> NotifAndPromotedContent(notificationKey, promotedContent) + is OngoingCallModel.InCallWithVisibleApp -> + // TODO(b/395989259): support InCallWithVisibleApp when it has notif data + null + is OngoingCallModel.NoCall -> null + } + + /** + * The top promoted notification represented by a chip, with the order determined by the order + * of the chips, not the notifications. + */ + private val topPromotedChipNotification: Flow<PromotedNotificationContentModel?> = + orderedChipNotifications + .map { list -> list.firstNotNullOfOrNull { it.promotedContent } } + .distinctUntilNewInstance() + + /** This is the top-most promoted notification, which should avoid regular changing. */ + val topPromotedNotificationContent: Flow<PromotedNotificationContentModel?> = + combine( + topPromotedChipNotification, + activeNotificationsInteractor.topLevelRepresentativeNotifications, + ) { topChipNotif, topLevelNotifs -> + topChipNotif ?: topLevelNotifs.firstNotNullOfOrNull { it.promotedContent } + } + // #equals() can be a bit expensive on this object, but this flow will regularly try to + // emit the same immutable instance over and over, so just prevent that. + .distinctUntilNewInstance() + + /** + * This is the ordered list of notifications (and the promoted content) represented as chips in + * the status bar. Flows on the background context. + */ + val orderedChipNotificationKeys: Flow<List<String>> = + orderedChipNotifications + .map { list -> list.map { it.key } } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + /** + * Returns flow where all subsequent repetitions of the same object instance are filtered out. + */ + private fun <T> Flow<T>.distinctUntilNewInstance() = distinctUntilChanged { a, b -> a === b } + + /** + * A custom pair, but providing clearer semantic names, and implementing equality as being the + * same instance of the promoted content model, which allows us to use distinctUntilChanged() on + * flows containing this without doing pixel comparisons on the Bitmaps inside Icon objects + * provided by the Notification. + */ + private data class NotifAndPromotedContent( + val key: String, + val promotedContent: PromotedNotificationContentModel?, + ) { + /** + * Define the equals of this object to only check the reference equality of the promoted + * content so that we can mark. + */ + override fun equals(other: Any?): Boolean { + return when { + other == null -> false + other === this -> true + other !is NotifAndPromotedContent -> return false + else -> key == other.key && promotedContent === other.promotedContent + } + } + + /** Define the hashCode to be very quick, even if it increases collisions. */ + override fun hashCode(): Int { + var result = key.hashCode() + result = 31 * result + (promotedContent?.identity?.hashCode() ?: 0) + return result + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt new file mode 100644 index 000000000000..c3b241154e0e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AsyncRowInflater.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import android.content.Context +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.annotation.UiThread +import com.android.app.tracing.coroutines.launchTraced +import com.android.app.tracing.coroutines.withContextTraced +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.NotifInflation +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job + +@SysUISingleton +class AsyncRowInflater +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Main private val mainCoroutineDispatcher: CoroutineDispatcher, + @NotifInflation private val inflationCoroutineDispatcher: CoroutineDispatcher, +) { + /** + * Inflate the layout on the background thread, and invoke the listener on the main thread when + * finished. + * + * If the inflation fails on the background, it will be retried once on the main thread. + */ + @UiThread + fun inflate( + context: Context, + layoutFactory: LayoutInflater.Factory2, + @LayoutRes resId: Int, + parent: ViewGroup, + listener: OnInflateFinishedListener, + ): Job { + val inflater = BasicRowInflater(context).apply { factory2 = layoutFactory } + return applicationScope.launchTraced("AsyncRowInflater-bg", inflationCoroutineDispatcher) { + val view = + try { + inflater.inflate(resId, parent, false) + } catch (ex: RuntimeException) { + // Probably a Looper failure, retry on the UI thread + Log.w( + "AsyncRowInflater", + "Failed to inflate resource in the background!" + + " Retrying on the UI thread", + ex, + ) + null + } + withContextTraced("AsyncRowInflater-ui", mainCoroutineDispatcher) { + // If the inflate failed on the inflation thread, try again on the main thread + val finalView = view ?: inflater.inflate(resId, parent, false) + // Inform the listener of the completion + listener.onInflateFinished(finalView, resId, parent) + } + } + } + + /** + * Callback interface (identical to the one from AsyncLayoutInflater) for receiving the inflated + * view + */ + interface OnInflateFinishedListener { + @UiThread fun onInflateFinished(view: View, @LayoutRes resId: Int, parent: ViewGroup?) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt new file mode 100644 index 000000000000..79d50b8398bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BasicRowInflater.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.row + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View + +/** + * A [LayoutInflater] that is copy of + * [androidx.asynclayoutinflater.view.AsyncLayoutInflater.BasicInflater] + */ +internal class BasicRowInflater(context: Context) : LayoutInflater(context) { + override fun cloneInContext(newContext: Context): LayoutInflater { + return BasicRowInflater(newContext) + } + + @Throws(ClassNotFoundException::class) + override fun onCreateView(name: String, attrs: AttributeSet): View { + for (prefix in sClassPrefixList) { + try { + val view = createView(name, prefix, attrs) + if (view != null) { + return view + } + } catch (e: ClassNotFoundException) { + // In this case we want to let the base class take a crack at it. + } + } + + return super.onCreateView(name, attrs) + } + + companion object { + private val sClassPrefixList = arrayOf("android.widget.", "android.webkit.", "android.app.") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 9bf07689dbdb..1568e9e66c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -105,6 +105,7 @@ import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.SourceType; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -120,6 +121,7 @@ import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedac import com.android.systemui.statusbar.notification.row.wrapper.NotificationCompactMessagingTemplateViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.shared.TransparentHeaderFix; import com.android.systemui.statusbar.notification.stack.AmbientState; @@ -268,6 +270,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private String mLoggingKey; private NotificationGuts mGuts; private NotificationEntry mEntry; + private EntryAdapter mEntryAdapter; private String mAppName; private NotificationRebindingTracker mRebindingTracker; private FalsingManager mFalsingManager; @@ -390,11 +393,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void toggleExpansionState(View v, boolean shouldLogExpandClickMetric) { - if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) - && mGroupMembershipManager.isGroupSummary(mEntry)) { + boolean isGroupRoot = NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) + : mGroupMembershipManager.isGroupSummary(mEntry); + if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) { mGroupExpansionChanging = true; - final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); - boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry); + final boolean wasExpanded = NotificationBundleUi.isEnabled() + ? mGroupExpansionManager.isGroupExpanded(mEntryAdapter) + : mGroupExpansionManager.isGroupExpanded(mEntry); + boolean nowExpanded = NotificationBundleUi.isEnabled() + ? mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter) + : mGroupExpansionManager.toggleGroupExpansion(mEntry); mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded); if (shouldLogExpandClickMetric) { mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded); @@ -910,6 +919,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return mEntry; } + @Nullable + public EntryAdapter getEntryAdapter() { + NotificationBundleUi.assertInNewMode(); + return mEntryAdapter; + } + @Override public boolean isHeadsUp() { return mIsHeadsUp; @@ -2010,11 +2025,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * * @param context context context of the view * @param attrs attributes used to initialize parent view + * @param user the user the row is associated to + */ + public ExpandableNotificationRow(Context context, AttributeSet attrs, UserHandle user) { + this(context, attrs, userContextForEntry(context, user)); + NotificationBundleUi.assertInNewMode(); + } + + /** + * Constructs an ExpandableNotificationRow. Used by layout inflation (with a custom {@code + * AsyncLayoutFactory} in {@link RowInflaterTask}. + * + * @param context context context of the view + * @param attrs attributes used to initialize parent view * @param entry notification that the row will be associated to (determines the user for the * ImageResolver) */ public ExpandableNotificationRow(Context context, AttributeSet attrs, NotificationEntry entry) { this(context, attrs, userContextForEntry(context, entry)); + NotificationBundleUi.assertInLegacyMode(); } private static Context userContextForEntry(Context base, NotificationEntry entry) { @@ -2025,6 +2054,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView UserHandle.of(entry.getSbn().getNormalizedUserId()), /* flags= */ 0); } + private static Context userContextForEntry(Context base, UserHandle user) { + if (base.getUserId() == user.getIdentifier()) { + return base; + } + return base.createContextAsUser(user, /* flags= */ 0); + } + private ExpandableNotificationRow(Context sysUiContext, AttributeSet attrs, Context userContext) { super(sysUiContext, attrs); @@ -2067,7 +2103,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView IStatusBarService statusBarService, UiEventLogger uiEventLogger, NotificationRebindingTracker notificationRebindingTracker) { - mEntry = entry; + + if (NotificationBundleUi.isEnabled()) { + // TODO (b/395857098): remove when all usages are migrated + mEntryAdapter = entry.getEntryAdapter(); + mEntry = entry; + } else { + mEntry = entry; + } mAppName = appName; mRebindingTracker = notificationRebindingTracker; if (mMenuRow == null) { @@ -2740,6 +2783,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView invalidateOutline(); mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight); + + if (Flags.notificationsLaunchRadius()) { + mBackgroundNormal.setRadius(params.getTopCornerRadius(), + params.getBottomCornerRadius()); + } } public void setExpandAnimationRunning(boolean expandAnimationRunning) { @@ -2871,7 +2919,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { if (mIsSummaryWithChildren && !shouldShowPublic() && allowChildExpansion && !mChildrenContainer.showingAsLowPriority()) { - final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); + final boolean wasExpanded = isGroupExpanded(); mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded); onExpansionChanged(true /* userAction */, wasExpanded); return; @@ -3026,6 +3074,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView @Override public boolean isGroupExpanded() { + if (NotificationBundleUi.isEnabled()) { + return mGroupExpansionManager.isGroupExpanded(mEntryAdapter); + } return mGroupExpansionManager.isGroupExpanded(mEntry); } @@ -3182,12 +3233,20 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } public void setSensitive(boolean sensitive, boolean hideSensitive) { + if (notificationsRedesignTemplates() + && sensitive == mSensitive && hideSensitive == mSensitiveHiddenInGeneral) { + return; // nothing has changed + } + int intrinsicBefore = getIntrinsicHeight(); mSensitive = sensitive; mSensitiveHiddenInGeneral = hideSensitive; int intrinsicAfter = getIntrinsicHeight(); if (intrinsicBefore != intrinsicAfter) { notifyHeightChanged(/* needsAnimation= */ true); + } else if (notificationsRedesignTemplates()) { + // Just request the correct layout, even if the height hasn't changed + getShowingLayout().requestSelectLayout(/* needsAnimation= */ true); } } @@ -3222,11 +3281,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } boolean oldShowingPublic = mShowingPublic; mShowingPublic = mSensitive && hideSensitive; - if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) { + boolean isShowingLayoutNotChanged = mShowingPublic == oldShowingPublic; + if (mShowingPublicInitialized && isShowingLayoutNotChanged) { return; } - if (!animated) { + final boolean shouldSkipHideSensitiveAnimation = + Flags.skipHideSensitiveNotifAnimation() && isShowingLayoutNotChanged; + if (!animated || shouldSkipHideSensitiveAnimation) { if (!NotificationContentAlphaOptimization.isEnabled() || mShowingPublic != oldShowingPublic) { // Don't reset the alpha or cancel the animation if the showing layout doesn't @@ -3761,7 +3823,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView public void onExpandedByGesture(boolean userExpanded) { int event = MetricsEvent.ACTION_NOTIFICATION_GESTURE_EXPANDER; - if (mGroupMembershipManager.isGroupSummary(mEntry)) { + if (NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isGroupRoot(mEntryAdapter) + : mGroupMembershipManager.isGroupSummary(mEntry)) { event = MetricsEvent.ACTION_NOTIFICATION_GROUP_GESTURE_EXPANDER; } mMetricsLogger.action(event, userExpanded); @@ -3797,7 +3861,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { - nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); + nowExpanded = isGroupExpanded(); } // Note: nowExpanded is going to be true here on the first expansion of minimized groups, // even though the group itself is not expanded. Use mGroupExpansionManager to get the real diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index e311b53bfa64..0c1dd2e026b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -56,6 +56,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.NmSummarizationUiFlag; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded; @@ -1457,12 +1458,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder } @Override - public void handleInflationException(NotificationEntry entry, Exception e) { + public void handleInflationException(Exception e) { handleError(e); } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { + public void onAsyncInflationFinished() { mEntry.onInflationTaskFinished(); mRow.onNotificationUpdated(); if (mCallback != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 430e5e4f1520..6e638f5de209 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -18,22 +18,13 @@ package com.android.systemui.statusbar.notification.row; import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; -import static android.service.notification.Adjustment.KEY_SUMMARIZATION; -import static android.service.notification.Adjustment.KEY_TYPE; -import static android.service.notification.NotificationAssistantService.ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS; -import static android.service.notification.NotificationAssistantService.EXTRA_NOTIFICATION_ADJUSTMENT; -import static android.service.notification.NotificationAssistantService.EXTRA_NOTIFICATION_KEY; -import android.annotation.FlaggedApi; import android.app.INotificationManager; import android.app.NotificationChannel; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.pm.ShortcutManager; import android.net.Uri; import android.os.Bundle; @@ -41,7 +32,6 @@ import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; -import android.service.notification.NotificationAssistantService; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.IconDrawableFactory; @@ -81,13 +71,14 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener; import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.wmshell.BubblesManager; -import java.util.List; import java.util.Optional; import javax.inject.Inject; @@ -131,6 +122,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta private final Optional<BubblesManager> mBubblesManagerOptional; private Runnable mOpenRunnable; private final INotificationManager mNotificationManager; + private final AppIconProvider mAppIconProvider; + private final NotificationIconStyleProvider mIconStyleProvider; private final PeopleSpaceWidgetManager mPeopleSpaceWidgetManager; private final UserManager mUserManager; @@ -154,6 +147,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta AccessibilityManager accessibilityManager, HighPriorityProvider highPriorityProvider, INotificationManager notificationManager, + AppIconProvider appIconProvider, + NotificationIconStyleProvider iconStyleProvider, UserManager userManager, PeopleSpaceWidgetManager peopleSpaceWidgetManager, LauncherApps launcherApps, @@ -180,6 +175,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta mAccessibilityManager = accessibilityManager; mHighPriorityProvider = highPriorityProvider; mNotificationManager = notificationManager; + mAppIconProvider = appIconProvider; + mIconStyleProvider = iconStyleProvider; mUserManager = userManager; mPeopleSpaceWidgetManager = peopleSpaceWidgetManager; mLauncherApps = launcherApps; @@ -427,6 +424,8 @@ public class NotificationGutsManager implements NotifGutsViewManager, CoreStarta notificationInfoView.bindNotification( pmUser, mNotificationManager, + mAppIconProvider, + mIconStyleProvider, mOnUserInteractionCallback, mChannelEditorDialogController, packageName, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java index 49b682d0a5d2..661122510c6c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row; +import static android.app.Flags.notificationsRedesignThemedAppIcons; import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO; import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; @@ -25,11 +26,13 @@ import static android.service.notification.Adjustment.KEY_SUMMARIZATION; import static android.service.notification.Adjustment.KEY_TYPE; import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.systemui.Flags.notificationsRedesignGuts; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.Flags; import android.app.INotificationManager; import android.app.Notification; @@ -70,8 +73,9 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.Dependency; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AssistantFeedbackController; -import com.android.systemui.statusbar.notification.NmSummarizationUiFlag; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; import java.lang.annotation.Retention; import java.util.List; @@ -88,6 +92,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G private TextView mAutomaticDescriptionView; private INotificationManager mINotificationManager; + private AppIconProvider mAppIconProvider; + private NotificationIconStyleProvider mIconStyleProvider; private OnUserInteractionCallback mOnUserInteractionCallback; private PackageManager mPm; private MetricsLogger mMetricsLogger; @@ -183,6 +189,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G public void bindNotification( PackageManager pm, INotificationManager iNotificationManager, + AppIconProvider appIconProvider, + NotificationIconStyleProvider iconStyleProvider, OnUserInteractionCallback onUserInteractionCallback, ChannelEditorDialogController channelEditorDialogController, String pkg, @@ -200,6 +208,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G OnClickListener onCloseClick) throws RemoteException { mINotificationManager = iNotificationManager; + mAppIconProvider = appIconProvider; + mIconStyleProvider = iconStyleProvider; mMetricsLogger = metricsLogger; mOnUserInteractionCallback = onUserInteractionCallback; mChannelEditorDialogController = channelEditorDialogController; @@ -290,23 +300,39 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G applyAlertingBehavior(behavior, false /* userTriggered */); } + @SuppressLint("WrongThread") private void bindHeader() { - // Package name mPkgIcon = null; // filled in if missing during notification inflation, which must have happened if // we have a notification to long press on ApplicationInfo info = mSbn.getNotification().extras.getParcelable(EXTRA_BUILDER_APPLICATION_INFO, ApplicationInfo.class); - if (info != null) { - try { - mAppName = String.valueOf(mPm.getApplicationLabel(info)); - mPkgIcon = mPm.getApplicationIcon(info); - } catch (Exception ignored) {} - } - if (mPkgIcon == null) { - // app is gone, just show package name and generic icon - mPkgIcon = mPm.getDefaultActivityIcon(); + if (notificationsRedesignGuts()) { + if (info != null) { + try { + mAppName = String.valueOf(mPm.getApplicationLabel(info)); + // The app icon is likely already in the cache, so let's use it + boolean withWorkProfileBadge = + mIconStyleProvider.shouldShowWorkProfileBadge(mSbn, getContext()); + mPkgIcon = mAppIconProvider.getOrFetchAppIcon(info.packageName, getContext(), + withWorkProfileBadge, + /* themed = */ notificationsRedesignThemedAppIcons()); + } catch (Exception ignored) { + } + } + } else { + if (info != null) { + try { + mAppName = String.valueOf(mPm.getApplicationLabel(info)); + mPkgIcon = mPm.getApplicationIcon(info); + } catch (Exception ignored) { + } + } + if (mPkgIcon == null) { + // app is gone, just show package name and generic icon + mPkgIcon = mPm.getDefaultActivityIcon(); + } } ((ImageView) findViewById(R.id.pkg_icon)).setImageDrawable(mPkgIcon); ((TextView) findViewById(R.id.pkg_name)).setText(mAppName); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index 0be1d5d9d79d..05934e7edfba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -24,6 +24,7 @@ import android.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.lang.annotation.Retention; @@ -170,13 +171,29 @@ public interface NotificationRowContentBinder { * @param entry notification which failed to inflate content * @param e exception */ - void handleInflationException(NotificationEntry entry, Exception e); + default void handleInflationException(NotificationEntry entry, Exception e) { + handleInflationException(e); + } + + /** + * Callback for when there is an inflation exception + * + * @param e exception + */ + void handleInflationException(Exception e); /** * Callback for after the content views finish inflating. * * @param entry the entry with the content views set */ - void onAsyncInflationFinished(NotificationEntry entry); + default void onAsyncInflationFinished(NotificationEntry entry) { + onAsyncInflationFinished(); + } + + /** + * Callback for after the content views finish inflating. + */ + void onAsyncInflationFinished(); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 517fc3a06d84..761d3fe91cd0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -49,6 +49,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException import com.android.systemui.statusbar.notification.NmSummarizationUiFlag +import com.android.systemui.statusbar.notification.collection.EntryAdapter import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel @@ -76,6 +77,7 @@ import com.android.systemui.statusbar.notification.row.shared.NotificationConten import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer import com.android.systemui.statusbar.policy.InflatedSmartReplyState import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder @@ -536,7 +538,7 @@ constructor( val ident: String = (sbn.packageName + "/0x" + Integer.toHexString(sbn.id)) Log.e(TAG, "couldn't inflate view for notification $ident", e) callback?.handleInflationException( - row.entry, + if (NotificationBundleUi.isEnabled) entry else row.entry, InflationException("Couldn't inflate contentViews$e"), ) @@ -554,11 +556,11 @@ constructor( logger.logAsyncTaskProgress(entry, "aborted") } - override fun handleInflationException(entry: NotificationEntry, e: Exception) { + override fun handleInflationException(e: Exception) { handleError(e) } - override fun onAsyncInflationFinished(entry: NotificationEntry) { + override fun onAsyncInflationFinished() { this.entry.onInflationTaskFinished() row.onNotificationUpdated() callback?.onAsyncInflationFinished(this.entry) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java index db25e0889298..6ff711deeb01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java @@ -31,6 +31,8 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; /** * The guts of a notification revealed when performing a long press, specifically @@ -50,6 +52,8 @@ public class PromotedNotificationInfo extends NotificationInfo { public void bindNotification( PackageManager pm, INotificationManager iNotificationManager, + AppIconProvider appIconProvider, + NotificationIconStyleProvider iconStyleProvider, OnUserInteractionCallback onUserInteractionCallback, ChannelEditorDialogController channelEditorDialogController, String pkg, @@ -64,11 +68,11 @@ public class PromotedNotificationInfo extends NotificationInfo { boolean wasShownHighPriority, AssistantFeedbackController assistantFeedbackController, MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException { - super.bindNotification(pm, iNotificationManager, onUserInteractionCallback, - channelEditorDialogController, pkg, notificationChannel, entry, onSettingsClick, - onAppSettingsClick, feedbackClickListener, uiEventLogger, isDeviceProvisioned, - isNonblockable, wasShownHighPriority, assistantFeedbackController, metricsLogger, - onCloseClick); + super.bindNotification(pm, iNotificationManager, appIconProvider, iconStyleProvider, + onUserInteractionCallback, channelEditorDialogController, pkg, notificationChannel, + entry, onSettingsClick, onAppSettingsClick, feedbackClickListener, uiEventLogger, + isDeviceProvisioned, isNonblockable, wasShownHighPriority, + assistantFeedbackController, metricsLogger, onCloseClick); mNotificationManager = iNotificationManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index 6883ec575d7e..da361406fa2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -21,10 +21,12 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon import androidx.annotation.NonNull; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.statusbar.notification.collection.EntryAdapter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import javax.inject.Inject; @@ -52,7 +54,7 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { @Override protected void executeStage( - @NonNull NotificationEntry entry, + final @NonNull NotificationEntry entry, @NonNull ExpandableNotificationRow row, @NonNull StageCallback callback) { RowContentBindParams params = getStageParams(entry); @@ -77,15 +79,35 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { InflationCallback inflationCallback = new InflationCallback() { @Override - public void handleInflationException(NotificationEntry entry, Exception e) { - mNotifInflationErrorManager.setInflationError(entry, e); + public void handleInflationException(NotificationEntry errorEntry, Exception e) { + if (NotificationBundleUi.isEnabled()) { + mNotifInflationErrorManager.setInflationError(entry, e); + } else { + mNotifInflationErrorManager.setInflationError(errorEntry, e); + } + } + + @Override + public void handleInflationException(Exception e) { + } @Override - public void onAsyncInflationFinished(NotificationEntry entry) { - mNotifInflationErrorManager.clearInflationError(entry); - getStageParams(entry).clearDirtyContentViews(); - callback.onStageFinished(entry); + public void onAsyncInflationFinished(NotificationEntry finishedEntry) { + if (NotificationBundleUi.isEnabled()) { + mNotifInflationErrorManager.clearInflationError(entry); + getStageParams(entry).clearDirtyContentViews(); + callback.onStageFinished(entry); + } else { + mNotifInflationErrorManager.clearInflationError(finishedEntry); + getStageParams(finishedEntry).clearDirtyContentViews(); + callback.onStageFinished(finishedEntry); + } + } + + @Override + public void onAsyncInflationFinished() { + } }; mBinder.cancelBind(entry, row); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java index 9f634bef4c5e..3971661fa787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import android.content.Context; +import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; @@ -29,9 +30,12 @@ import androidx.annotation.VisibleForTesting; import androidx.asynclayoutinflater.view.AsyncLayoutFactory; import androidx.asynclayoutinflater.view.AsyncLayoutInflater; +import com.android.systemui.Flags; import com.android.systemui.res.R; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.SystemClock; import java.util.concurrent.Executor; @@ -41,7 +45,8 @@ import javax.inject.Inject; /** * An inflater task that asynchronously inflates a ExpandableNotificationRow */ -public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInflateFinishedListener { +public class RowInflaterTask implements InflationTask, + AsyncLayoutInflater.OnInflateFinishedListener, AsyncRowInflater.OnInflateFinishedListener { private static final String TAG = "RowInflaterTask"; private static final boolean TRACE_ORIGIN = true; @@ -52,12 +57,17 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf private Throwable mInflateOrigin; private final SystemClock mSystemClock; private final RowInflaterTaskLogger mLogger; + private final AsyncRowInflater mAsyncRowInflater; private long mInflateStartTimeMs; + private UserTracker mUserTracker; @Inject - public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger) { + public RowInflaterTask(SystemClock systemClock, RowInflaterTaskLogger logger, + UserTracker userTracker, AsyncRowInflater asyncRowInflater) { mSystemClock = systemClock; mLogger = logger; + mUserTracker = userTracker; + mAsyncRowInflater = asyncRowInflater; } /** @@ -81,13 +91,19 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf mInflateOrigin = new Throwable("inflate requested here"); } mListener = listener; - AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, makeRowInflater(entry)); + RowAsyncLayoutInflater asyncLayoutFactory = makeRowInflater(entry); mEntry = entry; entry.setInflationTask(this); mLogger.logInflateStart(entry); mInflateStartTimeMs = mSystemClock.elapsedRealtime(); - inflater.inflate(R.layout.status_bar_notification_row, parent, listenerExecutor, this); + if (Flags.useNotifInflationThreadForRow()) { + mAsyncRowInflater.inflate(context, asyncLayoutFactory, + R.layout.status_bar_notification_row, parent, this); + } else { + AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, asyncLayoutFactory); + inflater.inflate(R.layout.status_bar_notification_row, parent, listenerExecutor, this); + } } /** @@ -107,40 +123,8 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf } private RowAsyncLayoutInflater makeRowInflater(NotificationEntry entry) { - return new RowAsyncLayoutInflater(entry, mSystemClock, mLogger); - } - - /** - * A {@link LayoutInflater} that is copy of BasicLayoutInflater. - */ - private static class BasicRowInflater extends LayoutInflater { - private static final String[] sClassPrefixList = - {"android.widget.", "android.webkit.", "android.app."}; - BasicRowInflater(Context context) { - super(context); - } - - @Override - public LayoutInflater cloneInContext(Context newContext) { - return new BasicRowInflater(newContext); - } - - @Override - protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { - for (String prefix : sClassPrefixList) { - try { - View view = createView(name, prefix, attrs); - if (view != null) { - return view; - } - } catch (ClassNotFoundException e) { - // In this case we want to let the base class take a crack - // at it. - } - } - - return super.onCreateView(name, attrs); - } + return new RowAsyncLayoutInflater( + entry, mSystemClock, mLogger, mUserTracker.getUserHandle()); } @VisibleForTesting @@ -148,12 +132,14 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf private final NotificationEntry mEntry; private final SystemClock mSystemClock; private final RowInflaterTaskLogger mLogger; + private final UserHandle mTargetUser; public RowAsyncLayoutInflater(NotificationEntry entry, SystemClock systemClock, - RowInflaterTaskLogger logger) { + RowInflaterTaskLogger logger, UserHandle targetUser) { mEntry = entry; mSystemClock = systemClock; mLogger = logger; + mTargetUser = targetUser; } @Nullable @@ -165,8 +151,12 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf } final long startMs = mSystemClock.elapsedRealtime(); - final ExpandableNotificationRow row = - new ExpandableNotificationRow(context, attrs, mEntry); + ExpandableNotificationRow row = null; + if (NotificationBundleUi.isEnabled()) { + row = new ExpandableNotificationRow(context, attrs, mTargetUser); + } else { + row = new ExpandableNotificationRow(context, attrs, mEntry); + } final long elapsedMs = mSystemClock.elapsedRealtime() - startMs; mLogger.logCreatedRow(mEntry, elapsedMs); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt index 52a0f6f92355..33d943348cb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt @@ -58,7 +58,7 @@ interface AppIconProvider { packageName: String, context: Context, withWorkProfileBadge: Boolean = false, - themed: Boolean = true, + themed: Boolean = false, ): Drawable /** 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 b9352bf64be4..3ee827332877 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 @@ -122,6 +122,7 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization; import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun; @@ -2958,9 +2959,13 @@ public class NotificationStackScrollLayout } private boolean isChildInGroup(View child) { - return child instanceof ExpandableNotificationRow - && mGroupMembershipManager.isChildInGroup( - ((ExpandableNotificationRow) child).getEntry()); + if (child instanceof ExpandableNotificationRow) { + ExpandableNotificationRow childRow = (ExpandableNotificationRow) child; + return NotificationBundleUi.isEnabled() + ? mGroupMembershipManager.isChildInGroup(childRow.getEntryAdapter()) + : mGroupMembershipManager.isChildInGroup(childRow.getEntry()); + } + return false; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 810d0b43b0dd..888c8cc59439 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -382,9 +382,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { // Only animate if in a non-sensitive state (not screen sharing) boolean shouldAnimate = animate && !isSensitiveContentProtectionActive; + mLogger.logUpdateSensitivenessWithAnimation(shouldAnimate, + isSensitive, + isSensitiveContentProtectionActive, + isAnyProfilePublic); mView.updateSensitiveness(shouldAnimate, isSensitive); } else { - mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); + boolean anyProfilePublicMode = mLockscreenUserManager.isAnyProfilePublicMode(); + mLogger.logUpdateSensitivenessWithAnimation(animate, anyProfilePublicMode); + mView.updateSensitiveness(animate, anyProfilePublicMode); } Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt index 3396306412bd..30658710c3c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt @@ -156,6 +156,44 @@ class NotificationStackScrollLogger @Inject constructor( { "removeTransientRow from NSSL: childKey: $str1" } ) } + + fun logUpdateSensitivenessWithAnimation( + shouldAnimate: Boolean, + isSensitive: Boolean, + isSensitiveContentProtectionActive: Boolean, + isAnyProfilePublic: Boolean, + ) { + notificationRenderBuffer.log( + TAG, + INFO, + { + bool1 = shouldAnimate + bool2 = isSensitive + bool3 = isSensitiveContentProtectionActive + bool4 = isAnyProfilePublic + }, + { + "updateSensitivenessWithAnimation from NSSL: shouldAnimate=$bool1 " + + "isSensitive(hideSensitive)=$bool2 isSensitiveContentProtectionActive=$bool3 " + + "isAnyProfilePublic=$bool4" + }, + ) + } + + fun logUpdateSensitivenessWithAnimation(animate: Boolean, anyProfilePublicMode: Boolean) { + notificationRenderBuffer.log( + TAG, + INFO, + { + bool1 = animate + bool2 = anyProfilePublicMode + }, + { + "updateSensitivenessWithAnimation from NSSL: animate=$bool1 " + + "anyProfilePublicMode(hideSensitive)=$bool2" + }, + ) + } } -private const val TAG = "NotificationStackScroll"
\ No newline at end of file +private const val TAG = "NotificationStackScroll" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 6385d53dbc8b..10b665d8ef01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -25,7 +25,7 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.logging.nano.MetricsProto import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo -import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.repeatWhenAttachedToWindow import com.android.systemui.plugins.FalsingManager @@ -76,7 +76,7 @@ import kotlinx.coroutines.flow.stateIn class NotificationListViewBinder @Inject constructor( - @Background private val backgroundDispatcher: CoroutineDispatcher, + @NotifInflation private val inflationDispatcher: CoroutineDispatcher, private val hiderTracker: DisplaySwitchNotificationsHiderTracker, @ShadeDisplayAware private val configuration: ConfigurationState, private val falsingManager: FalsingManager, @@ -155,7 +155,7 @@ constructor( parentView, attachToRoot = false, ) - .flowOn(backgroundDispatcher) + .flowOn(inflationDispatcher) .collectLatest { footerView: FooterView -> traceAsync("bind FooterView") { parentView.setFooterView(footerView) @@ -240,7 +240,7 @@ constructor( parentView, attachToRoot = false, ) - .flowOn(backgroundDispatcher) + .flowOn(inflationDispatcher) .collectLatest { emptyShadeView: EmptyShadeView -> traceAsync("bind EmptyShadeView") { parentView.setEmptyShadeView(emptyShadeView) 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 2c8c7a1bdd44..54efa4a2bcf2 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 @@ -48,7 +48,6 @@ import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.DozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel @@ -137,7 +136,6 @@ constructor( private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, private val aodToOccludedTransitionViewModel: AodToOccludedTransitionViewModel, private val aodToPrimaryBouncerTransitionViewModel: AodToPrimaryBouncerTransitionViewModel, - private val dozingToDreamingTransitionViewModel: DozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel: DozingToGlanceableHubTransitionViewModel, private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, @@ -574,7 +572,6 @@ constructor( aodToLockscreenTransitionViewModel.notificationAlpha, aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), aodToPrimaryBouncerTransitionViewModel.notificationAlpha, - dozingToDreamingTransitionViewModel.notificationAlpha, dozingToLockscreenTransitionViewModel.lockscreenAlpha, dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToPrimaryBouncerTransitionViewModel.notificationAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java index 1dc9de489806..05a46cd9fa31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java @@ -54,6 +54,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.kotlin.JavaAdapter; @@ -215,7 +216,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, if (ExpandHeadsUpOnInlineReply.isEnabled()) { if (row.isChildInGroup() && !row.areChildrenExpanded()) { // The group isn't expanded, let's make sure it's visible! - mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + if (NotificationBundleUi.isEnabled()) { + mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); + } else { + mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + } } else if (!row.isChildInGroup()) { final boolean expandNotification; if (row.isPinned()) { @@ -233,7 +238,11 @@ public class StatusBarRemoteInputCallback implements Callback, Callbacks, } else { if (row.isChildInGroup() && !row.areChildrenExpanded()) { // The group isn't expanded, let's make sure it's visible! - mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + if (NotificationBundleUi.isEnabled()) { + mGroupExpansionManager.toggleGroupExpansion(row.getEntryAdapter()); + } else { + mGroupExpansionManager.toggleGroupExpansion(row.getEntry()); + } } if (android.app.Flags.compactHeadsUpNotificationReply() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 788f041b38c0..0eef2e1ca685 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarV import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_ACTIVE import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants.ALPHA_INACTIVE +import com.android.systemui.util.kotlin.pairwiseBy import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged @@ -131,19 +132,37 @@ object MobileIconBinder { // Set the icon for the triangle launch { - viewModel.icon.distinctUntilChanged().collect { icon -> - viewModel.verboseLogger?.logBinderReceivedSignalIcon( - view, - viewModel.subscriptionId, - icon, - ) - if (icon is SignalIconModel.Cellular) { - iconView.setImageDrawable(mobileDrawable) - mobileDrawable.level = icon.toSignalDrawableState() - } else if (icon is SignalIconModel.Satellite) { - IconViewBinder.bind(icon.icon, iconView) + viewModel.icon + .pairwiseBy(initialValue = null) { oldIcon, newIcon -> + // Make sure we requestLayout if the number of levels changes + val shouldRequestLayout = + when { + oldIcon == null -> true + oldIcon is SignalIconModel.Cellular && + newIcon is SignalIconModel.Cellular -> { + oldIcon.numberOfLevels != newIcon.numberOfLevels + } + else -> false + } + Pair(shouldRequestLayout, newIcon) + } + .collect { (shouldRequestLayout, newIcon) -> + viewModel.verboseLogger?.logBinderReceivedSignalIcon( + view, + viewModel.subscriptionId, + newIcon, + ) + if (newIcon is SignalIconModel.Cellular) { + iconView.setImageDrawable(mobileDrawable) + mobileDrawable.level = newIcon.toSignalDrawableState() + } else if (newIcon is SignalIconModel.Satellite) { + IconViewBinder.bind(newIcon.icon, iconView) + } + + if (shouldRequestLayout) { + iconView.requestLayout() + } } - } } launch { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index fd5ab135a1ad..4458b224913b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -20,18 +20,16 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.ImageView -import com.android.settingslib.flags.Flags.newStatusBarIcons import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView.getVisibleStateString +import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView -class ModernStatusBarMobileView( - context: Context, - attrs: AttributeSet?, -) : ModernStatusBarView(context, attrs) { +class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) : + ModernStatusBarView(context, attrs) { var subId: Int = -1 @@ -62,9 +60,7 @@ class ModernStatusBarMobileView( as ModernStatusBarMobileView) .also { // Flag-specific configuration - if (newStatusBarIcons()) { - // New icon (with no embedded whitespace) is slightly shorter - // (but actually taller) + if (NewStatusBarIcons.isEnabled) { val iconView = it.requireViewById<ImageView>(R.id.mobile_signal) val lp = iconView.layoutParams lp.height = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index 9ad8619faacc..1d1826d532b7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.policy; import android.app.AlarmManager; -import android.app.Flags; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.ContentResolver; @@ -175,11 +174,7 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { @Override public void setZen(int zen, Uri conditionId, String reason) { - if (Flags.modesApi()) { - mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true); - } else { - mNoMan.setZenMode(zen, conditionId, reason); - } + mNoMan.setZenMode(zen, conditionId, reason, /* fromUser= */ true); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 28cf78f6777e..9f60fe212567 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -18,8 +18,10 @@ package com.android.systemui.theme; import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8; +import static com.android.systemui.Flags.hardwareColorStyles; import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP; +import static com.android.systemui.monet.ColorScheme.GOOGLE_BLUE; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK; import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET; @@ -73,6 +75,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.KeyguardState; @@ -99,6 +102,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -136,9 +140,11 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { private final DeviceProvisionedController mDeviceProvisionedController; private final Resources mResources; // Current wallpaper colors associated to a user. - private final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); + @VisibleForTesting + protected final SparseArray<WallpaperColors> mCurrentColors = new SparseArray<>(); private final WallpaperManager mWallpaperManager; private final ActivityManager mActivityManager; + protected final SystemPropertiesHelper mSystemPropertiesHelper; @VisibleForTesting protected ColorScheme mColorScheme; // If fabricated overlays were already created for the current theme. @@ -423,7 +429,9 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { JavaAdapter javaAdapter, KeyguardTransitionInteractor keyguardTransitionInteractor, UiModeManager uiModeManager, - ActivityManager activityManager) { + ActivityManager activityManager, + SystemPropertiesHelper systemPropertiesHelper + ) { mContext = context; mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET); mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY); @@ -443,6 +451,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mKeyguardTransitionInteractor = keyguardTransitionInteractor; mUiModeManager = uiModeManager; mActivityManager = activityManager; + mSystemPropertiesHelper = systemPropertiesHelper; dumpManager.registerDumpable(TAG, this); Flow<Boolean> isFinishedInAsleepStateFlow = mKeyguardTransitionInteractor @@ -498,29 +507,38 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor); mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); + WallpaperColors systemColor; + if (hardwareColorStyles() && !mDeviceProvisionedController.isCurrentUserSetup()) { + Pair<Integer, Color> defaultSettings = getThemeSettingsDefaults(); + mThemeStyle = defaultSettings.first; + Color seedColor = defaultSettings.second; + + // we only use the first color anyway, so we can pass only the single color we have + systemColor = new WallpaperColors( + /*primaryColor*/ seedColor, + /*secondaryColor*/ seedColor, + /*tertiaryColor*/ seedColor + ); + } else { + systemColor = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + } + // Upon boot, make sure we have the most up to date colors Runnable updateColors = () -> { - WallpaperColors systemColor = mWallpaperManager.getWallpaperColors( - getDefaultWallpaperColorsSource(mUserTracker.getUserId())); - Runnable applyColors = () -> { - if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); - mCurrentColors.put(mUserTracker.getUserId(), systemColor); - reevaluateSystemTheme(false /* forceReload */); - }; - if (mDeviceProvisionedController.isCurrentUserSetup()) { - mMainExecutor.execute(applyColors); - } else { - applyColors.run(); - } + if (DEBUG) Log.d(TAG, "Boot colors: " + systemColor); + mCurrentColors.put(mUserTracker.getUserId(), systemColor); + reevaluateSystemTheme(false /* forceReload */); }; // Whenever we're going directly to setup wizard, we need to process colors synchronously, // otherwise we'll see some jank when the activity is recreated. if (!mDeviceProvisionedController.isCurrentUserSetup()) { - updateColors.run(); + mMainExecutor.execute(updateColors); } else { mBgExecutor.execute(updateColors); } + mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null, UserHandle.USER_ALL); @@ -604,7 +622,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { @VisibleForTesting protected boolean isPrivateProfile(UserHandle userHandle) { - Context usercontext = mContext.createContextAsUser(userHandle,0); + Context usercontext = mContext.createContextAsUser(userHandle, 0); return usercontext.getSystemService(UserManager.class).isPrivateProfile(); } @@ -720,6 +738,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return true; } + @SuppressWarnings("StringCaseLocaleUsage") // Package name is not localized private void updateThemeOverlays() { final int currentUser = mUserTracker.getUserId(); final String overlayPackageJson = mSecureSettings.getStringForUser( @@ -746,7 +765,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { OverlayIdentifier systemPalette = categoryToPackage.get(OVERLAY_CATEGORY_SYSTEM_PALETTE); if (mIsMonetEnabled && systemPalette != null && systemPalette.getPackageName() != null) { try { - String colorString = systemPalette.getPackageName().toLowerCase(); + String colorString = systemPalette.getPackageName().toLowerCase(); if (!colorString.startsWith("#")) { colorString = "#" + colorString; } @@ -856,6 +875,75 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return style; } + protected Pair<Integer, String> getHardwareColorSetting() { + String deviceColorProperty = "ro.boot.hardware.color"; + + String[] themeData = mResources.getStringArray( + com.android.internal.R.array.theming_defaults); + + // Color can be hex (`#FF0000`) or `home_wallpaper` + Map<String, Pair<Integer, String>> themeMap = new HashMap<>(); + + // extract all theme settings + for (String themeEntry : themeData) { + String[] themeComponents = themeEntry.split("\\|"); + if (themeComponents.length != 3) continue; + themeMap.put(themeComponents[0], + new Pair<>(Style.valueOf(themeComponents[1]), themeComponents[2])); + } + + Pair<Integer, String> fallbackTheme = themeMap.get("*"); + if (fallbackTheme == null) { + Log.d(TAG, "Theming wildcard not found. Fallback to TONAL_SPOT|" + COLOR_SOURCE_HOME); + fallbackTheme = new Pair<>(Style.TONAL_SPOT, COLOR_SOURCE_HOME); + } + + String deviceColorPropertyValue = mSystemPropertiesHelper.get(deviceColorProperty); + Pair<Integer, String> selectedTheme = themeMap.get(deviceColorPropertyValue); + if (selectedTheme == null) { + Log.d(TAG, "Sysprop `" + deviceColorProperty + "` of value '" + deviceColorPropertyValue + + "' not found in theming_defaults: " + Arrays.toString(themeData)); + selectedTheme = fallbackTheme; + } + + return selectedTheme; + } + + @VisibleForTesting + protected Pair<Integer, Color> getThemeSettingsDefaults() { + + Pair<Integer, String> selectedTheme = getHardwareColorSetting(); + + // Last fallback color + Color defaultSeedColor = Color.valueOf(GOOGLE_BLUE); + + // defaultColor will come from wallpaper or be parsed from a string + boolean isWallpaper = selectedTheme.second.equals(COLOR_SOURCE_HOME); + + if (isWallpaper) { + WallpaperColors wallpaperColors = mWallpaperManager.getWallpaperColors( + getDefaultWallpaperColorsSource(mUserTracker.getUserId())); + + if (wallpaperColors != null) { + defaultSeedColor = wallpaperColors.getPrimaryColor(); + } + + Log.d(TAG, "Default seed color read from home wallpaper: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } else { + try { + defaultSeedColor = Color.valueOf(Color.parseColor(selectedTheme.second)); + Log.d(TAG, "Default seed color read from resource: " + Integer.toHexString( + defaultSeedColor.toArgb())); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error parsing color: " + selectedTheme.second, e); + // defaultSeedColor remains unchanged in this case + } + } + + return new Pair<>(selectedTheme.first, defaultSeedColor); + } + @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mSystemColors=" + mCurrentColors); diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt index e5c1e7daa25a..79ff38eabc08 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/SysUICoroutinesModule.kt @@ -21,6 +21,7 @@ import com.android.systemui.coroutines.newTracingContext import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.util.settings.SettingsSingleThreadBackground import dagger.Module @@ -123,4 +124,19 @@ class SysUICoroutinesModule { ): CoroutineContext { return uiBgCoroutineDispatcher } + + /** Coroutine dispatcher for background notification inflation. */ + @Provides + @NotifInflation + @SysUISingleton + fun notifInflationCoroutineDispatcher( + @NotifInflation notifInflationExecutor: Executor, + @Background bgCoroutineDispatcher: CoroutineDispatcher, + ): CoroutineDispatcher { + if (com.android.systemui.Flags.useNotifInflationThreadForFooter()) { + return notifInflationExecutor.asCoroutineDispatcher() + } else { + return bgCoroutineDispatcher + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt new file mode 100644 index 000000000000..4d6eb4d8f391 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SecureSettingsForUserRepository.kt @@ -0,0 +1,35 @@ +/* + * 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.util.settings.repository + +import android.provider.Settings +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineDispatcher + +/** Repository observing values of a [Settings.Secure] for the specified user. */ +@SysUISingleton +class SecureSettingsForUserRepository +@Inject +constructor( + secureSettings: SecureSettings, + @Background backgroundDispatcher: CoroutineDispatcher, + @Background backgroundContext: CoroutineContext, +) : SettingsForUserRepository(secureSettings, backgroundDispatcher, backgroundContext) diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt new file mode 100644 index 000000000000..94b3fd244a92 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/SettingsForUserRepository.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2025 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.util.settings.repository + +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.util.settings.UserSettingsProxy +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** + * Repository observing values of a [UserSettingsProxy] for the specified user. This repository + * should be used for any system that tracks the desired user internally (e.g. the Quick Settings + * tiles system). In other cases, use a [UserAwareSettingsRepository] instead. + */ +abstract class SettingsForUserRepository( + private val userSettings: UserSettingsProxy, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundContext: CoroutineContext, +) { + fun boolSettingForUser( + userId: Int, + name: String, + defaultValue: Boolean = false, + ): Flow<Boolean> = + settingObserver(name, userId) { userSettings.getBoolForUser(name, defaultValue, userId) } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + fun <T> settingObserver(name: String, userId: Int, settingsReader: () -> T): Flow<T> { + return userSettings + .observerFlow(userId, name) + .onStart { emit(Unit) } + .map { settingsReader.invoke() } + } + + suspend fun setBoolForUser(userId: Int, name: String, value: Boolean) { + withContext(backgroundContext) { userSettings.putBoolForUser(name, value, userId) } + } + + suspend fun getBoolForUser(userId: Int, name: String, defaultValue: Boolean = false): Boolean { + return withContext(backgroundContext) { + userSettings.getBoolForUser(name, defaultValue, userId) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt index 73329b467c04..a8068cda685b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt @@ -33,7 +33,8 @@ import kotlinx.coroutines.withContext /** * Repository for observing values of a [UserSettingsProxy], for the currently active user. That * means that when the user is switched and the new user has a different value, the flow will emit - * the new value. + * the new value. For any system that tracks the desired user internally (e.g. the Quick Settings + * tiles system), use a [SettingsForUserRepository] instead. */ // TODO: b/377244768 - Make internal when UserAwareSecureSettingsRepository can be made internal. abstract class UserAwareSettingsRepository( diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt index e2d2f3f68c6b..3efb2b464a1d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt @@ -16,7 +16,6 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel -import android.content.Context import com.android.internal.logging.UiEventLogger import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon @@ -24,6 +23,7 @@ import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R import com.android.systemui.volume.domain.interactor.AudioSharingInteractor +import com.android.systemui.volume.panel.shared.VolumePanelLogger import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -43,21 +44,25 @@ class AudioSharingStreamSliderViewModel @AssistedInject constructor( @Assisted private val coroutineScope: CoroutineScope, - private val context: Context, private val audioSharingInteractor: AudioSharingInteractor, private val uiEventLogger: UiEventLogger, private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, + private val volumePanelLogger: VolumePanelLogger, ) : SliderViewModel { private val volumeChanges = MutableStateFlow<Int?>(null) override val slider: StateFlow<SliderState> = - combine(audioSharingInteractor.volume, audioSharingInteractor.secondaryDevice) { - volume, - device -> + combine( + audioSharingInteractor.volume.distinctUntilChanged().onEach { + it?.let(volumePanelLogger::onAudioSharingVolumeUpdateReceived) + }, + audioSharingInteractor.secondaryDevice, + ) { volume, device -> val deviceName = device?.name ?: return@combine SliderState.Empty if (volume == null) { SliderState.Empty } else { + State( value = volume.toFloat(), valueRange = @@ -74,13 +79,15 @@ constructor( init { volumeChanges .filterNotNull() - .onEach { audioSharingInteractor.setStreamVolume(it) } + .onEach { + volumePanelLogger.onSetAudioSharingVolumeRequested(it) + audioSharingInteractor.setStreamVolume(it) + } .launchIn(coroutineScope) } override fun onValueChanged(state: SliderState, newValue: Float) { - val audioViewModel = state as? State - audioViewModel ?: return + if (state !is State) return volumeChanges.tryEmit(newValue.roundToInt()) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index 533276413ade..d74a433ad86c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession +import com.android.systemui.volume.panel.shared.VolumePanelLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -44,17 +45,23 @@ constructor( private val context: Context, private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, + private val volumePanelLogger: VolumePanelLogger, ) : SliderViewModel { override val slider: StateFlow<SliderState> = mediaDeviceSessionInteractor .playbackInfo(session) - .mapNotNull { it?.getCurrentState() } + .mapNotNull { + volumePanelLogger.onVolumeUpdateReceived(session.sessionToken, it.currentVolume) + it.getCurrentState() + } .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) override fun onValueChanged(state: SliderState, newValue: Float) { coroutineScope.launch { - mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt()) + val volume = newValue.roundToInt() + volumePanelLogger.onSetVolumeRequested(session.sessionToken, volume) + mediaDeviceSessionInteractor.setSessionVolume(session, volume) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt index 276326cbf430..930199a03a56 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt @@ -16,6 +16,8 @@ package com.android.systemui.volume.panel.shared +import android.media.session.MediaSession +import android.media.session.MediaSession.Token import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel @@ -42,7 +44,7 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo str1 = key bool1 = isAvailable }, - { "$str1 isAvailable=$bool1" } + { "$str1 isAvailable=$bool1" }, ) } @@ -51,7 +53,7 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo TAG, LogLevel.DEBUG, { bool1 = globalState.isVisible }, - { "Global state changed: isVisible=$bool1" } + { "Global state changed: isVisible=$bool1" }, ) } @@ -63,7 +65,7 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo str1 = audioStream.toString() int1 = volume }, - { "Set volume: stream=$str1 volume=$int1" } + { "Set volume: stream=$str1 volume=$int1" }, ) } @@ -75,7 +77,49 @@ class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: Lo str1 = audioStream.toString() int1 = volume }, - { "Volume update received: stream=$str1 volume=$int1" } + { "Volume update received: stream=$str1 volume=$int1" }, + ) + } + + fun onSetVolumeRequested(sessionToken: MediaSession.Token, volume: Int) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = sessionToken.toString() + int1 = volume + }, + { "Set volume: token=$str1 volume=$int1" }, + ) + } + + fun onVolumeUpdateReceived(sessionToken: Token, volume: Int) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = sessionToken.toString() + int1 = volume + }, + { "Volume update received: token=$str1 volume=$int1" }, + ) + } + + fun onSetAudioSharingVolumeRequested(volume: Int) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { int1 = volume }, + { "Set volume: audio-sharing volume=$int1" }, + ) + } + + fun onAudioSharingVolumeUpdateReceived(volume: Int) { + logBuffer.log( + TAG, + LogLevel.DEBUG, + { int1 = volume }, + { "Volume update received: audio-sharing volume=$int1" }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt index ec74f4f47bc9..300a7e070b6c 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt @@ -17,6 +17,8 @@ package com.android.systemui.wallpapers.data.repository import android.app.WallpaperInfo +import android.graphics.PointF +import android.graphics.RectF import android.view.View import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject @@ -37,4 +39,8 @@ class NoopWallpaperRepository @Inject constructor() : WallpaperRepository { override val wallpaperSupportsAmbientMode = flowOf(false) override var rootView: View? = null override val shouldSendFocalArea: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow() + + override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {} + + override fun sendTapCommand(tapPosition: PointF) {} } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt index 2c3491b06a90..974468c16578 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperFocalAreaRepository.kt @@ -33,7 +33,8 @@ interface WallpaperFocalAreaRepository { val wallpaperFocalAreaBounds: StateFlow<RectF> - val wallpaperFocalAreaTapPosition: StateFlow<PointF> + /** It will be true when wallpaper requires focal area info. */ + val hasFocalArea: StateFlow<Boolean> /** top of notifications without bcsmartspace in small clock settings */ val notificationDefaultTop: StateFlow<Float> @@ -51,7 +52,9 @@ interface WallpaperFocalAreaRepository { } @SysUISingleton -class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAreaRepository { +class WallpaperFocalAreaRepositoryImpl +@Inject +constructor(val wallpaperRepository: WallpaperRepository) : WallpaperFocalAreaRepository { private val _shortcutAbsoluteTop = MutableStateFlow(0F) override val shortcutAbsoluteTop = _shortcutAbsoluteTop.asStateFlow() @@ -63,13 +66,11 @@ class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAre override val wallpaperFocalAreaBounds: StateFlow<RectF> = _wallpaperFocalAreaBounds.asStateFlow() - private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F)) - override val wallpaperFocalAreaTapPosition: StateFlow<PointF> = - _wallpaperFocalAreaTapPosition.asStateFlow() - private val _notificationDefaultTop = MutableStateFlow(0F) override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow() + override val hasFocalArea = wallpaperRepository.shouldSendFocalArea + override fun setShortcutAbsoluteTop(top: Float) { _shortcutAbsoluteTop.value = top } @@ -84,9 +85,10 @@ class WallpaperFocalAreaRepositoryImpl @Inject constructor() : WallpaperFocalAre override fun setWallpaperFocalAreaBounds(bounds: RectF) { _wallpaperFocalAreaBounds.value = bounds + wallpaperRepository.sendLockScreenLayoutChangeCommand(bounds) } - override fun setTapPosition(point: PointF) { - _wallpaperFocalAreaTapPosition.value = point + override fun setTapPosition(tapPosition: PointF) { + wallpaperRepository.sendTapCommand(tapPosition) } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt index a55f76b333d9..b07342c4c76d 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt @@ -21,22 +21,18 @@ import android.app.WallpaperManager import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.graphics.PointF +import android.graphics.RectF import android.os.Bundle import android.os.UserHandle import android.provider.Settings +import android.util.Log import android.view.View -import androidx.annotation.VisibleForTesting -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background -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.TransitionState import com.android.systemui.res.R as SysUIR -import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.shared.Flags.extendedWallpaperEffects import com.android.systemui.user.data.model.SelectedUserModel @@ -48,7 +44,6 @@ import com.android.systemui.utils.coroutines.flow.mapLatestConflated import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -76,6 +71,10 @@ interface WallpaperRepository { /** some wallpapers require bounds to be sent from keyguard */ val shouldSendFocalArea: StateFlow<Boolean> + + fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) + + fun sendTapCommand(tapPosition: PointF) } @SysUISingleton @@ -86,10 +85,8 @@ constructor( @Background private val bgDispatcher: CoroutineDispatcher, broadcastDispatcher: BroadcastDispatcher, userRepository: UserRepository, - wallpaperFocalAreaRepository: WallpaperFocalAreaRepository, private val wallpaperManager: WallpaperManager, private val context: Context, - keyguardTransitionInteractor: KeyguardTransitionInteractor, private val secureSettings: SecureSettings, ) : WallpaperRepository { private val wallpaperChanged: Flow<Unit> = @@ -109,9 +106,6 @@ constructor( // Only update the wallpaper status once the user selection has finished. .filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE } - @VisibleForTesting var sendLockscreenLayoutJob: Job? = null - @VisibleForTesting var sendTapInShapeEffectsJob: Job? = null - override val wallpaperInfo: StateFlow<WallpaperInfo?> = if (!wallpaperManager.isWallpaperSupported) { MutableStateFlow(null).asStateFlow() @@ -143,77 +137,45 @@ constructor( override var rootView: View? = null + override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) { + if (DEBUG) { + Log.d(TAG, "sendLockScreenLayoutChangeCommand $wallpaperFocalAreaBounds") + } + wallpaperManager.sendWallpaperCommand( + /* windowToken = */ rootView?.windowToken, + /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_LAYOUT_CHANGED, + /* x = */ 0, + /* y = */ 0, + /* z = */ 0, + /* extras = */ Bundle().apply { + putFloat("wallpaperFocalAreaLeft", wallpaperFocalAreaBounds.left) + putFloat("wallpaperFocalAreaRight", wallpaperFocalAreaBounds.right) + putFloat("wallpaperFocalAreaTop", wallpaperFocalAreaBounds.top) + putFloat("wallpaperFocalAreaBottom", wallpaperFocalAreaBounds.bottom) + }, + ) + } + + override fun sendTapCommand(tapPosition: PointF) { + if (DEBUG) { + Log.d(TAG, "sendTapCommand $tapPosition") + } + + wallpaperManager.sendWallpaperCommand( + /* windowToken = */ rootView?.windowToken, + /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP, + /* x = */ tapPosition.x.toInt(), + /* y = */ tapPosition.y.toInt(), + /* z = */ 0, + /* extras = */ Bundle(), + ) + } + override val shouldSendFocalArea = wallpaperInfo .map { val focalAreaTarget = context.resources.getString(SysUIR.string.focal_area_target) val shouldSendNotificationLayout = it?.component?.className == focalAreaTarget - if (shouldSendNotificationLayout) { - sendLockscreenLayoutJob = - scope.launch { - combine( - wallpaperFocalAreaRepository.wallpaperFocalAreaBounds, - keyguardTransitionInteractor - .transition( - edge = Edge.create(to = Scenes.Lockscreen), - edgeWithoutSceneContainer = - Edge.create(to = KeyguardState.LOCKSCREEN), - ) - .filter { transitionStep -> - transitionStep.transitionState == - TransitionState.STARTED - }, - ::Pair, - ) - .map { (bounds, _) -> bounds } - .collect { wallpaperFocalAreaBounds -> - wallpaperManager.sendWallpaperCommand( - /* windowToken = */ rootView?.windowToken, - /* action = */ WallpaperManager - .COMMAND_LOCKSCREEN_LAYOUT_CHANGED, - /* x = */ 0, - /* y = */ 0, - /* z = */ 0, - /* extras = */ Bundle().apply { - putFloat( - "wallpaperFocalAreaLeft", - wallpaperFocalAreaBounds.left, - ) - putFloat( - "wallpaperFocalAreaRight", - wallpaperFocalAreaBounds.right, - ) - putFloat( - "wallpaperFocalAreaTop", - wallpaperFocalAreaBounds.top, - ) - putFloat( - "wallpaperFocalAreaBottom", - wallpaperFocalAreaBounds.bottom, - ) - }, - ) - } - } - - sendTapInShapeEffectsJob = - scope.launch { - wallpaperFocalAreaRepository.wallpaperFocalAreaTapPosition.collect { - wallpaperFocalAreaTapPosition -> - wallpaperManager.sendWallpaperCommand( - /* windowToken = */ rootView?.windowToken, - /* action = */ WallpaperManager.COMMAND_LOCKSCREEN_TAP, - /* x = */ wallpaperFocalAreaTapPosition.x.toInt(), - /* y = */ wallpaperFocalAreaTapPosition.y.toInt(), - /* z = */ 0, - /* extras = */ null, - ) - } - } - } else { - sendLockscreenLayoutJob?.cancel() - sendTapInShapeEffectsJob?.cancel() - } shouldSendNotificationLayout } .stateIn( @@ -227,4 +189,9 @@ constructor( wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id) } } + + companion object { + private val TAG = WallpaperRepositoryImpl::class.simpleName + private val DEBUG = true + } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt index 187d6c7801c0..09c6cdf0ce22 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt @@ -20,60 +20,38 @@ import android.content.Context import android.content.res.Resources import android.graphics.PointF import android.graphics.RectF +import android.util.Log import android.util.TypedValue -import androidx.annotation.VisibleForTesting import com.android.app.animation.MathUtils import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.wallpapers.data.repository.WallpaperFocalAreaRepository -import com.android.systemui.wallpapers.data.repository.WallpaperRepository import javax.inject.Inject import kotlin.math.min -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter @SysUISingleton class WallpaperFocalAreaInteractor @Inject constructor( - @Application private val applicationScope: CoroutineScope, private val context: Context, private val wallpaperFocalAreaRepository: WallpaperFocalAreaRepository, shadeRepository: ShadeRepository, - activeNotificationsInteractor: ActiveNotificationsInteractor, - val wallpaperRepository: WallpaperRepository, ) { - // When there's notifications in splitshade, the focal area should be left aligned - @VisibleForTesting - val notificationInShadeWideLayout: Flow<Boolean> = - combine( - shadeRepository.isShadeLayoutWide, - activeNotificationsInteractor.areAnyNotificationsPresent, - ) { isShadeLayoutWide, areAnyNotificationsPresent: Boolean -> - when { - !isShadeLayoutWide -> false - !areAnyNotificationsPresent -> false - else -> true - } - } - - val hasFocalArea = wallpaperRepository.shouldSendFocalArea + val hasFocalArea = wallpaperFocalAreaRepository.hasFocalArea val wallpaperFocalAreaBounds: Flow<RectF> = combine( shadeRepository.isShadeLayoutWide, - notificationInShadeWideLayout, wallpaperFocalAreaRepository.notificationStackAbsoluteBottom, wallpaperFocalAreaRepository.shortcutAbsoluteTop, wallpaperFocalAreaRepository.notificationDefaultTop, ) { isShadeLayoutWide, - notificationInShadeWideLayout, notificationStackAbsoluteBottom, shortcutAbsoluteTop, notificationDefaultTop -> @@ -97,28 +75,21 @@ constructor( screenBounds.centerY() + screenBounds.height() / 2F / wallpaperZoomedInScale, ) + val focalAreaMaxWidthDp = getFocalAreaMaxWidthDp(context) val maxFocalAreaWidth = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, - FOCAL_AREA_MAX_WIDTH_DP.toFloat(), + focalAreaMaxWidthDp.toFloat(), context.resources.displayMetrics, ) val (left, right) = - // tablet landscape - if (context.resources.getBoolean(R.bool.center_align_focal_area_shape)) { + // Tablet & unfold foldable landscape + if (isShadeLayoutWide) { Pair( scaledBounds.centerX() - maxFocalAreaWidth / 2F, scaledBounds.centerX() + maxFocalAreaWidth / 2F, ) - // unfold foldable landscape - } else if (isShadeLayoutWide) { - if (notificationInShadeWideLayout) { - Pair(scaledBounds.left, scaledBounds.centerX()) - } else { - Pair(scaledBounds.centerX(), scaledBounds.right) - } - // handheld / portrait } else { val focalAreaWidth = min(scaledBounds.width(), maxFocalAreaWidth) Pair( @@ -147,8 +118,10 @@ constructor( wallpaperZoomedInScale } val bottom = scaledBounds.bottom - scaledBottomMargin - RectF(left, top, right, bottom) + RectF(left, top, right, bottom).also { Log.d(TAG, "Focal area changes to $it") } } + // Make sure a valid rec + .filter { it.width() >= 0 && it.height() >= 0 } .distinctUntilChanged() fun setFocalAreaBounds(bounds: RectF) { @@ -187,8 +160,17 @@ constructor( return if (scale == 0f) 1f else scale } - // A max width for focal area shape effects bounds, to avoid - // it becoming too large in large screen portrait mode - const val FOCAL_AREA_MAX_WIDTH_DP = 500 + // A max width for focal area shape effects bounds, to avoid it becoming too large, + // especially in portrait mode + const val FOCAL_AREA_MAX_WIDTH_DP_TABLET = 500 + const val FOCAL_AREA_MAX_WIDTH_DP_FOLDABLE = 400 + + fun getFocalAreaMaxWidthDp(context: Context): Int { + return if (context.resources.getBoolean(R.bool.center_align_focal_area_shape)) + FOCAL_AREA_MAX_WIDTH_DP_TABLET + else FOCAL_AREA_MAX_WIDTH_DP_FOLDABLE + } + + private val TAG = WallpaperFocalAreaInteractor::class.simpleName } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt index 70a97d473c49..4cd49d03ad36 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt @@ -17,15 +17,41 @@ package com.android.systemui.wallpapers.ui.viewmodel import android.graphics.RectF +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.TransitionState +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor import javax.inject.Inject +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map class WallpaperFocalAreaViewModel @Inject -constructor(private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor) { +constructor( + private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, + val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) { val hasFocalArea = wallpaperFocalAreaInteractor.hasFocalArea - val wallpaperFocalAreaBounds = wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds + val wallpaperFocalAreaBounds = + combine( + wallpaperFocalAreaInteractor.wallpaperFocalAreaBounds, + keyguardTransitionInteractor + .transition( + edge = Edge.create(to = Scenes.Lockscreen), + edgeWithoutSceneContainer = Edge.create(to = KeyguardState.LOCKSCREEN), + ) + .filter { transitionStep -> + // Should not filter by TransitionState.STARTED, it may race with + // wakingup command, causing layout change command not be received. + transitionStep.transitionState == TransitionState.FINISHED + }, + ::Pair, + ) + .map { (bounds, _) -> bounds } fun setFocalAreaBounds(bounds: RectF) { wallpaperFocalAreaInteractor.setFocalAreaBounds(bounds) diff --git a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt index 153df7f29737..06532bc0cc2a 100644 --- a/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/window/ui/WindowRootViewBinder.kt @@ -84,9 +84,16 @@ object WindowRootViewBinder { combine(viewModel.blurRadius, viewModel.isBlurOpaque, ::Pair) .filter { it.first >= 0 } .collect { (blurRadius, isOpaque) -> - // Expectation is that we schedule only one blur radius value - // per frame + val newBlurRadius = blurRadius.toInt() + // Expectation is that we schedule only one frame callback per frame if (wasUpdateScheduledForThisFrame) { + // Update this value so that the frame callback picks up this + // value when it runs + if (lastScheduledBlurRadius != newBlurRadius) { + Log.w(TAG, "Multiple blur values emitted in the same frame") + } + lastScheduledBlurRadius = newBlurRadius + lastScheduleBlurOpaqueness = isOpaque return@collect } TrackTracer.instantForGroup( @@ -94,7 +101,7 @@ object WindowRootViewBinder { "preparedBlurRadius", blurRadius, ) - lastScheduledBlurRadius = blurRadius.toInt() + lastScheduledBlurRadius = newBlurRadius lastScheduleBlurOpaqueness = isOpaque wasUpdateScheduledForThisFrame = true blurUtils.prepareBlur( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt b/packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt index 8635bb0e8ab2..8635bb0e8ab2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/OnTeardownRuleTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java index 6d75c4ca3a38..6d75c4ca3a38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java index 4553f983b898..45b9f4ad2322 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java @@ -552,12 +552,23 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { mockSeekBar, OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER); - // should trigger callback to update magnifier scale and persist the scale + // Should trigger callback to update magnifier scale and persist the scale. verify(mWindowMagnificationSettingsCallback) .onMagnifierScale(/* scale= */ eq(4f), /* updatePersistence= */ eq(true)); } @Test + public void onSeekbarUserInteractionFinalized_notFromUser_persistedScaleNotUpdated() { + OnSeekBarWithIconButtonsChangeListener onChangeListener = + mZoomSeekbar.getOnSeekBarWithIconButtonsChangeListener(); + onChangeListener.onProgressChanged(mZoomSeekbar.getSeekbar(), 30, false); + + // Should not trigger callback to update magnifier scale and persist the scale. + verify(mWindowMagnificationSettingsCallback, never()) + .onMagnifierScale(/* scale= */ anyFloat(), /* updatePersistence= */ eq(true)); + } + + @Test public void seekbarProgress_scaleUpdatedAfterSettingPanelOpened_progressAlsoUpdated() { setupMagnificationCapabilityAndMode( /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_ALL, diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt index dcf38800bb01..14a81b3f8bfb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TextAnimatorTest.kt @@ -30,7 +30,6 @@ import kotlin.math.ceil import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Mockito.eq import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -60,10 +59,11 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } - textAnimator.setTextStyle(weight = 400, animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), TextAnimator.Animation()) // If animation is requested, the base state should be rebased and the target state should // be updated. @@ -90,10 +90,11 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } - textAnimator.setTextStyle(weight = 400, animate = false) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400")) // If animation is not requested, the progress should be 1 which is end of animation and the // base state is rebased to target state by calling rebase. @@ -118,23 +119,24 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } textAnimator.setTextStyle( - weight = 400, - animate = true, - onAnimationEnd = animationEndCallback, + TextAnimator.Style("'wght' 400"), + TextAnimator.Animation(animate = true, onAnimationEnd = animationEndCallback), ) // Verify animationEnd callback has been added. val captor = ArgumentCaptor.forClass(AnimatorListenerAdapter::class.java) - verify(valueAnimator).addListener(captor.capture()) - captor.value.onAnimationEnd(valueAnimator) + verify(valueAnimator, times(2)).addListener(captor.capture()) + for (callback in captor.allValues) { + callback.onAnimationEnd(valueAnimator) + } // Verify animationEnd callback has been invoked and removed. verify(animationEndCallback).run() - verify(valueAnimator).removeListener(eq(captor.value)) } @Test @@ -148,18 +150,20 @@ class TextAnimatorTest : SysuiTestCase() { val textAnimator = TextAnimator(layout, TypefaceVariantCacheImpl(typeface, 20)).apply { this.textInterpolator = textInterpolator + this.createAnimator = { valueAnimator } this.animator = valueAnimator } - textAnimator.setTextStyle(weight = 400, animate = true) + val animation = TextAnimator.Animation(animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation) val prevTypeface = paint.typeface - textAnimator.setTextStyle(weight = 700, animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 700"), animation) assertThat(paint.typeface).isNotSameInstanceAs(prevTypeface) - textAnimator.setTextStyle(weight = 400, animate = true) + textAnimator.setTextStyle(TextAnimator.Style("'wght' 400"), animation) assertThat(paint.typeface).isSameInstanceAs(prevTypeface) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index b6c63479990e..b6c63479990e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt index b4200b6850c8..b4200b6850c8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 296a0fc2eb40..296a0fc2eb40 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt index ae6b337a3fa0..ae6b337a3fa0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/LogEulogizerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogEulogizerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt index 6cb6fed978b8..6cb6fed978b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt index 9c7f01495b58..9c7f01495b58 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt index b0e93fbecbb9..b0e93fbecbb9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index a2bd5ec28f08..aaf5559290df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -66,6 +66,7 @@ import com.android.systemui.scene.data.repository.Idle import com.android.systemui.scene.data.repository.setSceneTransition import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -203,6 +204,7 @@ class MediaCarouselControllerTest(flags: FlagsParameterization) : SysuiTestCase( mediaCarouselViewModel = kosmos.mediaCarouselViewModel, mediaViewControllerFactory = mediaViewControllerFactory, deviceEntryInteractor = kosmos.deviceEntryInteractor, + mediaControlChipInteractor = kosmos.mediaControlChipInteractor, ) verify(configurationController).addCallback(capture(configListener)) verify(visualStabilityProvider) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 23282b16d8a8..205ccea657df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -84,7 +84,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { private final Kosmos mKosmos = SysuiTestCaseExtKt.testKosmos(this); // Mock - private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class); + private MediaOutputAdapterBase mMediaOutputBaseAdapter = mock(MediaOutputAdapterBase.class); private MediaController mMediaController = mock(MediaController.class); private PlaybackState mPlaybackState = mock(PlaybackState.class); private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class); @@ -219,7 +219,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { public void refresh_withIconCompat_iconIsVisible() { mIconCompat = IconCompat.createWithBitmap( Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)); - when(mMediaOutputBaseAdapter.getController()).thenReturn(mMediaSwitchingController); mMediaOutputBaseDialogImpl.refresh(); final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt index ab78029684d4..ab78029684d4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/QSFragmentComposeTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt index 1a4749c3196c..1a4749c3196c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/sensorprivacy/SensorUseStartedActivityTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java index 49cbb5a924f1..49cbb5a924f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt index 89a3d5b5cf0b..89a3d5b5cf0b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shadow/DoubleShadowTextClockTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt index e076418f2630..79e78c9532c6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt @@ -20,9 +20,10 @@ import android.view.LayoutInflater import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.animation.Interpolators -import com.android.systemui.customization.R import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.FontVariationUtils import com.android.systemui.animation.TextAnimator +import com.android.systemui.customization.R import com.android.systemui.util.mockito.any import org.junit.Before import org.junit.Rule @@ -32,7 +33,9 @@ import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.eq @RunWith(AndroidJUnit4::class) @SmallTest @@ -46,6 +49,7 @@ class AnimatableClockViewTest : SysuiTestCase() { @Before fun setUp() { val layoutInflater = LayoutInflater.from(context) + whenever(mockTextAnimator.fontVariationUtils).thenReturn(FontVariationUtils()) clockView = layoutInflater.inflate(R.layout.clock_default_small, null) as AnimatableClockView clockView.textAnimatorFactory = { _, _ -> mockTextAnimator } @@ -57,18 +61,19 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.animateAppearOnLockscreen() clockView.measure(50, 50) + verify(mockTextAnimator).fontVariationUtils verify(mockTextAnimator).glyphFilter = any() verify(mockTextAnimator) .setTextStyle( - weight = 300, - textSize = -1.0f, - color = 200, - strokeWidth = -1F, - animate = false, - duration = 833L, - interpolator = Interpolators.EMPHASIZED_DECELERATE, - delay = 0L, - onAnimationEnd = null + eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)), + eq( + TextAnimator.Animation( + animate = false, + duration = 833L, + interpolator = Interpolators.EMPHASIZED_DECELERATE, + onAnimationEnd = null, + ) + ), ) verifyNoMoreInteractions(mockTextAnimator) } @@ -79,30 +84,24 @@ class AnimatableClockViewTest : SysuiTestCase() { clockView.measure(50, 50) clockView.animateAppearOnLockscreen() + verify(mockTextAnimator, times(2)).fontVariationUtils verify(mockTextAnimator, times(2)).glyphFilter = any() verify(mockTextAnimator) .setTextStyle( - weight = 100, - textSize = -1.0f, - color = 200, - strokeWidth = -1F, - animate = false, - duration = 0L, - interpolator = null, - delay = 0L, - onAnimationEnd = null + eq(TextAnimator.Style(fVar = "'wght' 100", color = 200)), + eq(TextAnimator.Animation(animate = false, duration = 0)), ) + verify(mockTextAnimator) .setTextStyle( - weight = 300, - textSize = -1.0f, - color = 200, - strokeWidth = -1F, - animate = true, - duration = 833L, - interpolator = Interpolators.EMPHASIZED_DECELERATE, - delay = 0L, - onAnimationEnd = null + eq(TextAnimator.Style(fVar = "'wght' 300", color = 200)), + eq( + TextAnimator.Animation( + animate = true, + duration = 833L, + interpolator = Interpolators.EMPHASIZED_DECELERATE, + ) + ), ) verifyNoMoreInteractions(mockTextAnimator) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt index 8418598c256b..8418598c256b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/CombinedConditionTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt index d67ce303c451..d67ce303c451 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/UncaughtExceptionPreHandlerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt index fcbf0fe9a37a..fcbf0fe9a37a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt index 04319f05f6f9..04319f05f6f9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java index 281ce16b539f..19d1224a9bf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java @@ -28,6 +28,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; import static com.android.systemui.statusbar.NotificationEntryHelper.modifySbn; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -46,6 +48,7 @@ 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.SetFlagsRule; import android.service.notification.NotificationListenerService.Ranking; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; @@ -59,9 +62,12 @@ import com.android.systemui.statusbar.RankingBuilder; import com.android.systemui.statusbar.SbnBuilder; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -83,6 +89,9 @@ public class NotificationEntryTest extends SysuiTestCase { private NotificationChannel mChannel = Mockito.mock(NotificationChannel.class); private final FakeSystemClock mClock = new FakeSystemClock(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setup() { Notification.Builder n = new Notification.Builder(mContext, "") @@ -444,6 +453,145 @@ public class NotificationEntryTest extends SysuiTestCase { // no crash, good } + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getParent_adapter() { + GroupEntry ge = new GroupEntryBuilder() + .build(); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(ge) + .build(); + + assertThat(entry.getEntryAdapter().getParent()).isEqualTo(entry.getParent()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void isTopLevelEntry_adapter() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + + assertThat(entry.getEntryAdapter().isTopLevelEntry()).isTrue(); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getKey_adapter() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + + assertThat(entry.getEntryAdapter().getKey()).isEqualTo(entry.getKey()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getRow_adapter() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getRow()).isEqualTo(entry.getRow()); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getGroupRoot_adapter_groupSummary() { + ExpandableNotificationRow row = mock(ExpandableNotificationRow.class); + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build(); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + entry.setRow(row); + + assertThat(entry.getEntryAdapter().getGroupRoot()).isNull(); + } + + @Test + @EnableFlags(NotificationBundleUi.FLAG_NAME) + public void getGroupRoot_adapter_groupChild() { + Notification notification = new Notification.Builder(mContext, "") + .setSmallIcon(R.drawable.ic_person) + .setGroupSummary(true) + .setGroup("key") + .build(); + + NotificationEntry parent = new NotificationEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .build(); + GroupEntryBuilder groupEntry = new GroupEntryBuilder() + .setSummary(parent); + + NotificationEntry entry = new NotificationEntryBuilder() + .setPkg(TEST_PACKAGE_NAME) + .setOpPkg(TEST_PACKAGE_NAME) + .setUid(TEST_UID) + .setChannel(mChannel) + .setId(mId++) + .setNotification(notification) + .setUser(new UserHandle(ActivityManager.getCurrentUser())) + .setParent(groupEntry.build()) + .build(); + + assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter()); + } private Notification.Action createContextualAction(String title) { return new Notification.Action.Builder( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index f1edb417a314..f1edb417a314 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt index 99dcd6c9a798..99dcd6c9a798 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 77b116e2e465..a6722c5f4c22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static android.app.Flags.FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES; + import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.PKG; import static com.android.systemui.statusbar.notification.row.NotificationTestHelper.USER_HANDLE; @@ -29,6 +31,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -189,6 +192,54 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES) + public void setSensitive_doesNothingIfCalledAgain() throws Exception { + ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + measureAndLayout(row); + + // GIVEN a mocked public layout + NotificationContentView mockPublicLayout = mock(NotificationContentView.class); + row.setPublicLayout(mockPublicLayout); + + // GIVEN a sensitive notification row that's currently redacted + row.setHideSensitiveForIntrinsicHeight(true); + row.setSensitive(true, true); + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout()); + verify(mockPublicLayout).requestSelectLayout(eq(true)); + clearInvocations(mockPublicLayout); + + // WHEN the row is set to the same sensitive settings + row.setSensitive(true, true); + + // VERIFY that the layout is not updated again + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout()); + verify(mockPublicLayout, never()).requestSelectLayout(anyBoolean()); + } + + @Test + @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES) + public void testSetSensitiveOnNotifRowUpdatesLayout() throws Exception { + // GIVEN a sensitive notification row that's currently redacted + ExpandableNotificationRow row = mNotificationTestHelper.createRow(); + measureAndLayout(row); + row.setHideSensitiveForIntrinsicHeight(true); + row.setSensitive(true, true); + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPublicLayout()); + + // GIVEN a mocked private layout + NotificationContentView mockPrivateLayout = mock(NotificationContentView.class); + row.setPrivateLayout(mockPrivateLayout); + + // WHEN the row is set to no longer be sensitive + row.setSensitive(false, true); + + // VERIFY that the layout is updated + assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout()); + verify(mockPrivateLayout).requestSelectLayout(eq(true)); + } + + @Test + @DisableFlags(FLAG_NOTIFICATIONS_REDESIGN_TEMPLATES) public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception { // GIVEN a sensitive notification row that's currently redacted ExpandableNotificationRow row = mNotificationTestHelper.createRow(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt index 699e8c30afde..47238fedee4d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -23,6 +23,7 @@ import android.service.notification.StatusBarNotification import android.testing.TestableLooper import android.testing.ViewUtils import android.view.NotificationHeaderView +import android.view.NotificationTopLineView import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -37,6 +38,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.notification.FeedbackIcon import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.shared.NotificationBundleUi import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -82,8 +84,21 @@ class NotificationContentViewTest : SysuiTestCase() { val mockEntry = createMockNotificationEntry() row = spy( - ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply { - entry = mockEntry + when (NotificationBundleUi.isEnabled) { + true -> { + ExpandableNotificationRow( + mContext, + /* attrs= */ null, + UserHandle.CURRENT + ).apply { + entry = mockEntry + } + } + false -> { + ExpandableNotificationRow(mContext, /* attrs= */ null, mockEntry).apply { + entry = mockEntry + } + } } ) ViewUtils.attachView(fakeParent) @@ -270,7 +285,7 @@ class NotificationContentViewTest : SysuiTestCase() { val icon = FeedbackIcon( R.drawable.ic_feedback_alerted, - R.string.notification_feedback_indicator_alerted + R.string.notification_feedback_indicator_alerted, ) view.setFeedbackIcon(icon) @@ -291,10 +306,7 @@ class NotificationContentViewTest : SysuiTestCase() { val mockHeadsUpEB = mock<NotificationExpandButton>() val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB) - val view = - createContentView( - isSystemExpanded = false, - ) + val view = createContentView(isSystemExpanded = false) // Update all 3 child forms view.apply { @@ -319,12 +331,14 @@ class NotificationContentViewTest : SysuiTestCase() { private fun createMockNotificationHeaderView( height: Int, - mockExpandedEB: NotificationExpandButton + mockExpandedEB: NotificationExpandButton, ) = spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height }) .apply { whenever(this.animate()).thenReturn(mock()) whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB) + whenever(this.findViewById<View>(R.id.notification_top_line)) + .thenReturn(mock<NotificationTopLineView>()) } @Test @@ -344,7 +358,7 @@ class NotificationContentViewTest : SysuiTestCase() { isSystemExpanded = false, contractedView = mockContracted, expandedView = mockExpanded, - headsUpView = mockHeadsUp + headsUpView = mockHeadsUp, ) view.setRemoteInputVisible(true) @@ -373,7 +387,7 @@ class NotificationContentViewTest : SysuiTestCase() { isSystemExpanded = false, contractedView = mockContracted, expandedView = mockExpanded, - headsUpView = mockHeadsUp + headsUpView = mockHeadsUp, ) view.setRemoteInputVisible(false) @@ -635,7 +649,7 @@ class NotificationContentViewTest : SysuiTestCase() { contractedView: View = createViewWithHeight(contractedHeight), expandedView: View = createViewWithHeight(expandedHeight), headsUpView: View = createViewWithHeight(contractedHeight), - row: ExpandableNotificationRow = this.row + row: ExpandableNotificationRow = this.row, ): NotificationContentView { val height = if (isSystemExpanded) expandedHeight else contractedHeight doReturn(height).whenever(row).intrinsicHeight @@ -647,7 +661,7 @@ class NotificationContentViewTest : SysuiTestCase() { setHeights( /* smallHeight= */ contractedHeight, /* headsUpMaxHeight= */ contractedHeight, - /* maxHeight= */ expandedHeight + /* maxHeight= */ expandedHeight, ) contractedChild = contractedView expandedChild = expandedView diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index 1b447525bbf5..3d4c90140adb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -67,6 +67,8 @@ import com.android.systemui.statusbar.notification.collection.provider.HighPrior import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.headsup.mockHeadsUpManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.icon.appIconProvider +import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.statusbar.policy.deviceProvisionedController @@ -128,6 +130,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { private val statusBarStateController = kosmos.statusBarStateController private val headsUpManager = kosmos.mockHeadsUpManager private val activityStarter = kosmos.activityStarter + private val appIconProvider = kosmos.appIconProvider + private val iconStyleProvider = kosmos.notificationIconStyleProvider private val userManager = kosmos.userManager private val activeNotificationsInteractor = kosmos.activeNotificationsInteractor private val sceneInteractor = kosmos.sceneInteractor @@ -174,6 +178,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { accessibilityManager, highPriorityProvider, notificationManager, + appIconProvider, + iconStyleProvider, userManager, peopleSpaceWidgetManager, launcherApps, @@ -429,6 +435,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { .bindNotification( any<PackageManager>(), any<INotificationManager>(), + eq(appIconProvider), + eq(iconStyleProvider), eq(onUserInteractionCallback), eq(channelEditorDialogController), eq(statusBarNotification.packageName), @@ -463,6 +471,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { .bindNotification( any<PackageManager>(), any<INotificationManager>(), + eq(appIconProvider), + eq(iconStyleProvider), eq(onUserInteractionCallback), eq(channelEditorDialogController), eq(statusBarNotification.packageName), @@ -497,6 +507,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { .bindNotification( any<PackageManager>(), any<INotificationManager>(), + eq(appIconProvider), + eq(iconStyleProvider), eq(onUserInteractionCallback), eq(channelEditorDialogController), eq(statusBarNotification.packageName), @@ -529,8 +541,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { .setChannel(testNotificationChannel) .build() row - } catch (e: Exception) { - org.junit.Assert.fail() + } catch (_: Exception) { + Assert.fail() null } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt index 86689cb88569..86689cb88569 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowImageInflaterTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index 31f8590c0378..31f8590c0378 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 14a1233045bb..10886760b521 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -63,6 +63,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; import com.android.systemui.dock.DockManager; +import com.android.systemui.flags.DisableSceneContainer; import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository; @@ -118,10 +119,8 @@ public class ScrimControllerTest extends SysuiTestCase { @Rule public Expect mExpect = Expect.create(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); - private final FakeConfigurationController mConfigurationController = - new FakeConfigurationController(); - private final LargeScreenShadeInterpolator - mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); + private FakeConfigurationController mConfigurationController; + private LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator; private final TestScope mTestScope = mKosmos.getTestScope(); private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); @@ -137,6 +136,7 @@ public class ScrimControllerTest extends SysuiTestCase { private boolean mAlwaysOnEnabled; private TestableLooper mLooper; private Context mContext; + @Mock private DozeParameters mDozeParameters; @Mock private LightBarController mLightBarController; @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory; @@ -149,12 +149,11 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; @Mock private AlternateBouncerToGoneTransitionViewModel mAlternateBouncerToGoneTransitionViewModel; - private final KeyguardTransitionInteractor mKeyguardTransitionInteractor = - mKosmos.getKeyguardTransitionInteractor(); - private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository = - mKosmos.getKeyguardTransitionRepository(); @Mock private KeyguardInteractor mKeyguardInteractor; + private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private FakeKeyguardTransitionRepository mKeyguardTransitionRepository; + // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The // event-dispatch-on-registration pattern caused some of these unit tests to fail.) @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @@ -238,6 +237,9 @@ public class ScrimControllerTest extends SysuiTestCase { when(mContext.getColor(com.android.internal.R.color.materialColorSurface)) .thenAnswer(invocation -> mSurfaceColor); + mConfigurationController = new FakeConfigurationController(); + mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); + mScrimBehind = spy(new ScrimView(mContext)); mScrimInFront = new ScrimView(mContext); mNotificationsScrim = new ScrimView(mContext); @@ -270,6 +272,9 @@ public class ScrimControllerTest extends SysuiTestCase { when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha()) .thenReturn(emptyFlow()); + mKeyguardTransitionRepository = mKosmos.getKeyguardTransitionRepository(); + mKeyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor(); + mScrimController = new ScrimController( mLightBarController, mDozeParameters, @@ -322,6 +327,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -337,6 +343,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToShadeLocked() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); @@ -373,6 +380,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToShadeLocked_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -391,6 +399,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToOff() { mScrimController.legacyTransitionTo(ScrimState.OFF); finishAnimationsImmediately(); @@ -406,6 +415,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_withRegularWallpaper() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -421,6 +431,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_withFrontAlphaUpdates() { // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -465,6 +476,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToAod_afterDocked_ignoresAlwaysOnAndUpdatesFrontAlpha() { // Assert that setting the AOD front scrim alpha doesn't take effect in a non-AOD state. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -506,6 +518,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToPulsing_withFrontAlphaUpdates() { // Pre-condition // Need to go to AoD first because PULSING doesn't change @@ -551,6 +564,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguardBouncer() { mScrimController.legacyTransitionTo(BOUNCER); finishAnimationsImmediately(); @@ -571,6 +585,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void lockscreenToHubTransition_setsBehindScrimAlpha() { // Start on lockscreen. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -617,6 +632,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void hubToLockscreenTransition_setsViewAlpha() { // Start on glanceable hub. mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -663,6 +679,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToHub() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -677,6 +694,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openBouncerOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -706,6 +724,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openShadeOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -734,6 +753,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToHubOverDream() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -748,6 +768,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openBouncerOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -777,6 +798,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void openShadeOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -805,6 +827,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() { assertEquals(BOUNCER.getBehindTint(), 0x112233); mSurfaceColor = 0x223344; @@ -813,6 +836,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void onThemeChangeWhileClipQsScrim_bouncerBehindTint_remainsBlack() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -825,6 +849,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToKeyguardBouncer_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -845,6 +870,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -867,6 +893,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(BOUNCER); @@ -889,6 +916,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToBouncer() { mScrimController.legacyTransitionTo(ScrimState.BOUNCER_SCRIMMED); finishAnimationsImmediately(); @@ -902,6 +930,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlocked_clippedQs() { mScrimController.setClipsQsScrim(true); mScrimController.setRawPanelExpansionFraction(0f); @@ -960,6 +989,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() { mScrimController.setClipsQsScrim(false); mScrimController.setRawPanelExpansionFraction(0f); @@ -999,6 +1029,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimStateCallback() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1014,6 +1045,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void panelExpansion() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1036,6 +1068,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion() { reset(mScrimBehind); mScrimController.setQsPosition(1f, 999 /* value doesn't matter */); @@ -1048,6 +1081,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1061,6 +1095,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_half_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1074,6 +1109,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void panelExpansionAffectsAlpha() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1096,6 +1132,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlockedFromOff() { // Simulate unlock with fingerprint without AOD mScrimController.legacyTransitionTo(ScrimState.OFF); @@ -1118,6 +1155,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToUnlockedFromAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1140,6 +1178,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimBlanksBeforeLeavingAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1163,6 +1202,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void scrimBlankCallbackWhenUnlockingFromPulse() { boolean[] blanked = {false}; // Simulate unlock with fingerprint @@ -1181,6 +1221,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void blankingNotRequired_leavingAoD() { // GIVEN display does NOT need blanking when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); @@ -1236,6 +1277,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimCallback() { int[] callOrder = {0, 0, 0}; int[] currentCall = {0}; @@ -1262,12 +1304,14 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimCallbacksWithoutAmbientDisplay() { mAlwaysOnEnabled = false; testScrimCallback(); } @Test + @DisableSceneContainer public void testScrimCallbackCancelled() { boolean[] cancelledCalled = {false}; mScrimController.legacyTransitionTo(ScrimState.AOD, new ScrimController.Callback() { @@ -1281,6 +1325,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testHoldsWakeLock_whenAOD() { mScrimController.legacyTransitionTo(ScrimState.AOD); verify(mWakeLock).acquire(anyString()); @@ -1290,6 +1335,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoesNotHoldWakeLock_whenUnlocking() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1297,6 +1343,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testCallbackInvokedOnSameStateTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1306,6 +1353,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testConservesExpansionOpacityAfterTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1323,6 +1371,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testCancelsOldAnimationBeforeBlanking() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -1335,6 +1384,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsAreNotFocusable() { assertFalse("Behind scrim should not be focusable", mScrimBehind.isFocusable()); assertFalse("Front scrim should not be focusable", mScrimInFront.isFocusable()); @@ -1343,6 +1393,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testEatsTouchEvent() { HashSet<ScrimState> eatsTouches = new HashSet<>(Collections.singletonList(ScrimState.AOD)); @@ -1359,6 +1410,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testAnimatesTransitionToAod() { when(mDozeParameters.shouldControlScreenOff()).thenReturn(false); ScrimState.AOD.prepare(ScrimState.KEYGUARD); @@ -1373,6 +1425,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testIsLowPowerMode() { HashSet<ScrimState> lowPowerModeStates = new HashSet<>(Arrays.asList( ScrimState.OFF, ScrimState.AOD, ScrimState.PULSING)); @@ -1390,6 +1443,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsOpaque_whenShadeFullyExpanded() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(1); @@ -1404,6 +1458,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisible() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1419,6 +1474,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoesntAnimate_whenUnlocking() { // LightRevealScrim will animate the transition, we should only hide the keyguard scrims. ScrimState.UNLOCKED.prepare(ScrimState.KEYGUARD); @@ -1439,6 +1495,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisible_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1454,6 +1511,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testScrimsVisible_whenShadeVisibleOnLockscreen() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); mScrimController.setQsPosition(0.25f, 300); @@ -1465,6 +1523,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationScrimTransparent_whenOnLockscreen() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); // even if shade is not pulled down, panel has expansion of 1 on the lockscreen @@ -1477,6 +1536,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() { mScrimController.setRawPanelExpansionFraction(1); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -1488,6 +1548,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void qsExpansion_BehindTint_shadeLocked_bouncerActive_usesBouncerProgress() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); // clipping doesn't change tested logic but allows to assert scrims more in line with @@ -1504,6 +1565,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); @@ -1520,6 +1582,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1535,6 +1598,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1554,6 +1618,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); mScrimController.setClipsQsScrim(true); @@ -1574,6 +1639,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(true); @@ -1594,6 +1660,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(false); @@ -1605,6 +1672,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotificationTransparency_followsTransitionToFullShade() { mScrimController.setClipsQsScrim(true); @@ -1646,6 +1714,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationTransparency_followsNotificationScrimProgress() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setRawPanelExpansionFraction(1.0f); @@ -1662,6 +1731,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_qsNotClipped_alphaMatchesNotificationExpansionProgress() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1697,6 +1767,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_setsTranslationYOnNotificationsScrim() { int overScrollAmount = 10; @@ -1706,6 +1777,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnBehindScrim() { int overScrollAmount = 10; @@ -1715,6 +1787,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnFrontScrim() { int overScrollAmount = 10; @@ -1724,6 +1797,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationBoundsTopGetsPassedToKeyguard() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); @@ -1734,6 +1808,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1744,6 +1819,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void transitionToDreaming() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -1763,6 +1839,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void keyguardGoingAwayUpdateScrims() { when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); mScrimController.updateScrims(); @@ -1772,6 +1849,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Test + @DisableSceneContainer public void setUnOccludingAnimationKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -1786,6 +1864,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testHidesScrimFlickerInActivity() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1804,6 +1883,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_clipsQsScrimFalse() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1813,6 +1893,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void aodStateSetsFrontScrimToNotBlend() { mScrimController.legacyTransitionTo(ScrimState.AOD); assertFalse("Front scrim should not blend with main color", @@ -1820,6 +1901,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void applyState_unlocked_bouncerShowing() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setBouncerHiddenFraction(0.99f); @@ -1829,6 +1911,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void ignoreTransitionRequestWhileKeyguardTransitionRunning() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.mBouncerToGoneTransition.accept( @@ -1841,6 +1924,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() { when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true); mScrimController.mBouncerToGoneTransition.accept( @@ -1851,6 +1935,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsLightBarController() { reset(mLightBarController); mScrimController.mBouncerToGoneTransition.accept( @@ -1862,6 +1947,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testDoNotAnimateChangeIfOccludeAnimationPlaying() { mScrimController.setOccludeAnimationPlaying(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1870,6 +1956,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true); @@ -1942,9 +2029,9 @@ public class ScrimControllerTest extends SysuiTestCase { // Check combined scrim visibility. final int visibility; - if (scrimToAlpha.values().contains(OPAQUE)) { + if (scrimToAlpha.containsValue(OPAQUE)) { visibility = OPAQUE; - } else if (scrimToAlpha.values().contains(SEMI_TRANSPARENT)) { + } else if (scrimToAlpha.containsValue(SEMI_TRANSPARENT)) { visibility = SEMI_TRANSPARENT; } else { visibility = TRANSPARENT; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index dde6e2ee1866..dde6e2ee1866 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt index a230f0630d6e..a230f0630d6e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TouchableRegionViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt index 1135a5f86952..1135a5f86952 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt index a1122c3cbcd2..a1122c3cbcd2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt index c7b685fba455..c7b685fba455 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProviderTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt index b93c161a7039..b93c161a7039 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/progress/UnfoldRemoteFilterTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt index 32c598612aa6..32c598612aa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt index 25ceea951d3c..25ceea951d3c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/CreateUserActivityTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/CreateUserActivityTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt index 9440280649dd..9440280649dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt index b3f2113f86ec..b3f2113f86ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/PackageManagerExtComponentEnabledTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java index c896fc0bfb8a..c896fc0bfb8a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/AsyncSensorManagerTest.java diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt index b4fbaad6ab37..b4fbaad6ab37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/GradientColorWallpaperTest.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt index a6e71333c816..5dc28bea9bc4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt @@ -17,33 +17,77 @@ package com.android.systemui.activity.data.repository import android.app.activityManager +import com.android.systemui.activity.data.model.AppVisibilityModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.core.Logger +import com.android.systemui.util.time.SystemClock +import com.android.systemui.util.time.fakeSystemClock +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -val Kosmos.activityManagerRepository by Kosmos.Fixture { FakeActivityManagerRepository() } +val Kosmos.activityManagerRepository by + Kosmos.Fixture { FakeActivityManagerRepository(fakeSystemClock) } val Kosmos.realActivityManagerRepository by - Kosmos.Fixture { ActivityManagerRepositoryImpl(testDispatcher, activityManager) } + Kosmos.Fixture { + ActivityManagerRepositoryImpl(testDispatcher, fakeSystemClock, activityManager) + } -class FakeActivityManagerRepository : ActivityManagerRepository { - private val uidFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>() +class FakeActivityManagerRepository(private val systemClock: SystemClock) : + ActivityManagerRepository { + private val isVisibleFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>() + private val appVisibilityFlows = + mutableMapOf<Int, MutableList<MutableStateFlow<AppVisibilityModel>>>() var startingIsAppVisibleValue = false + override fun createAppVisibilityFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<AppVisibilityModel> { + val newFlow = + MutableStateFlow( + if (startingIsAppVisibleValue) { + AppVisibilityModel( + isAppCurrentlyVisible = true, + lastAppVisibleTime = systemClock.currentTimeMillis(), + ) + } else { + AppVisibilityModel(isAppCurrentlyVisible = false, lastAppVisibleTime = null) + } + ) + appVisibilityFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow) + return newFlow + } + override fun createIsAppVisibleFlow( creationUid: Int, logger: Logger, identifyingLogTag: String, ): MutableStateFlow<Boolean> { val newFlow = MutableStateFlow(startingIsAppVisibleValue) - uidFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow) + isVisibleFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow) return newFlow } fun setIsAppVisible(uid: Int, isAppVisible: Boolean) { - uidFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible } + isVisibleFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible } + appVisibilityFlows[uid]?.forEach { stateFlow -> + stateFlow.value = + if (isAppVisible) { + AppVisibilityModel( + isAppCurrentlyVisible = true, + lastAppVisibleTime = systemClock.currentTimeMillis(), + ) + } else { + AppVisibilityModel( + isAppCurrentlyVisible = false, + stateFlow.value.lastAppVisibleTime, + ) + } + } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt index 4068a2290559..b781f61723f2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt @@ -20,7 +20,7 @@ import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -32,7 +32,7 @@ val Kosmos.keyguardClockInteractor by mediaCarouselInteractor = mediaCarouselInteractor, activeNotificationsInteractor = activeNotificationsInteractor, aodPromotedNotificationInteractor = aodPromotedNotificationInteractor, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, headsUpNotificationInteractor = headsUpNotificationInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt index c0b39b1df7d5..5dc19a340dd0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt @@ -22,7 +22,7 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.ui.systemBarUtilsProxy @@ -33,7 +33,7 @@ val Kosmos.keyguardClockViewModel by keyguardClockInteractor = keyguardClockInteractor, applicationScope = applicationCoroutineScope, aodNotificationIconViewModel = notificationIconContainerAlwaysOnDisplayViewModel, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, systemBarUtils = systemBarUtilsProxy, configurationInteractor = configurationInteractor, resources = mainResources, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt index 16d3fdc26613..345d69aa8df0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt @@ -19,12 +19,17 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor val Kosmos.keyguardMediaViewModelFactory by Kosmos.Fixture { object : KeyguardMediaViewModel.Factory { override fun create(): KeyguardMediaViewModel { - return KeyguardMediaViewModel(mediaCarouselInteractor, keyguardInteractor) + return KeyguardMediaViewModel( + mediaCarouselInteractor = mediaCarouselInteractor, + keyguardInteractor = keyguardInteractor, + shadeModeInteractor = shadeModeInteractor, + ) } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt index dd13b8b143ae..b751e213152e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt @@ -25,7 +25,7 @@ import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimati import com.android.systemui.keyguard.shared.transition.keyguardTransitionAnimationCallbackDelegator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor val Kosmos.lockscreenContentViewModelFactory by Fixture { @@ -38,7 +38,7 @@ val Kosmos.lockscreenContentViewModelFactory by Fixture { interactor = keyguardBlueprintInteractor, authController = authController, touchHandling = keyguardTouchHandlingViewModel, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, unfoldTransitionInteractor = unfoldTransitionInteractor, deviceEntryInteractor = deviceEntryInteractor, transitionInteractor = keyguardTransitionInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt index 60c0f342b874..f9917ac680e0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt @@ -44,6 +44,8 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : var pendingOverlays: Set<OverlayKey>? = null private set + var freezeAndAnimateToCurrentStateCallCount = 0 + override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { if (_isPaused) { _pendingScene = toScene @@ -85,6 +87,10 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : hideOverlay(overlay) } + override fun freezeAndAnimateToCurrentState() { + freezeAndAnimateToCurrentStateCallCount++ + } + /** * Pauses scene and overlay changes. * diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt index 67dd0ad896d5..0892e66b8b86 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTrackerKosmos.kt @@ -27,7 +27,7 @@ import java.util.Optional val Kosmos.shadeDisplayChangeLatencyTracker by Fixture { ShadeDisplayChangeLatencyTracker( - Optional.of(mockShadeRootView), + mockShadeRootView, configurationRepository, latencyTracker, testScope.backgroundScope, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt index 46314135c574..1397d974cbc5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt @@ -29,7 +29,6 @@ import com.android.systemui.statusbar.notification.domain.interactor.activeNotif import com.android.systemui.statusbar.notification.row.notificationRebindingTracker import com.android.systemui.statusbar.notification.stack.notificationStackRebindingHider import com.android.systemui.statusbar.policy.configurationController -import java.util.Optional import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -55,11 +54,11 @@ val Kosmos.shadeDisplaysInteractor by testScope.backgroundScope, testScope.backgroundScope.coroutineContext, mockedShadeDisplayChangeLatencyTracker, - Optional.of(shadeExpandedStateInteractor), + shadeExpandedStateInteractor, shadeExpansionIntent, activeNotificationsInteractor, notificationRebindingTracker, - Optional.of(notificationStackRebindingHider), + notificationStackRebindingHider, configurationController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt index 20e4523fda0f..55e35f2b2703 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt @@ -18,9 +18,11 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory @@ -31,6 +33,8 @@ val Kosmos.notificationsShadeOverlayContentViewModel: notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory, sceneInteractor = sceneInteractor, shadeInteractor = shadeInteractor, + disableFlagsInteractor = disableFlagsInteractor, + mediaCarouselInteractor = mediaCarouselInteractor, activeNotificationsInteractor = activeNotificationsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt index 878c2deb43b2..d8e0cfe4fbf8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor +import com.android.systemui.util.time.fakeSystemClock val Kosmos.notifChipsViewModel: NotifChipsViewModel by Kosmos.Fixture { @@ -29,5 +30,6 @@ val Kosmos.notifChipsViewModel: NotifChipsViewModel by applicationCoroutineScope, statusBarNotificationChipsInteractor, headsUpNotificationInteractor, + fakeSystemClock, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt new file mode 100644 index 000000000000..59f5ecd2563f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationEntryBuilderKosmos.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.content.applicationContext +import android.graphics.drawable.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.icon.IconPack +import com.android.systemui.statusbar.notification.promoted.setPromotedContent +import org.mockito.kotlin.mock + +fun Kosmos.setIconPackWithMockIconViews(entry: NotificationEntry) { + entry.icons = + IconPack.buildPack( + /* statusBarIcon = */ mock(), + /* statusBarChipIcon = */ mock(), + /* shelfIcon = */ mock(), + /* aodIcon = */ mock(), + /* source = */ null, + ) +} + +fun Kosmos.buildOngoingCallEntry( + promoted: Boolean = false, + block: NotificationEntryBuilder.() -> Unit = {}, +): NotificationEntry = + buildNotificationEntry( + tag = "call", + promoted = promoted, + style = makeOngoingCallStyle(), + block = block, + ) + +fun Kosmos.buildPromotedOngoingEntry( + block: NotificationEntryBuilder.() -> Unit = {} +): NotificationEntry = + buildNotificationEntry(tag = "ron", promoted = true, style = null, block = block) + +fun Kosmos.buildNotificationEntry( + tag: String? = null, + promoted: Boolean = false, + style: Notification.Style? = null, + block: NotificationEntryBuilder.() -> Unit = {}, +): NotificationEntry = + NotificationEntryBuilder() + .apply { + setTag(tag) + setFlag(applicationContext, Notification.FLAG_PROMOTED_ONGOING, promoted) + modifyNotification(applicationContext) + .setSmallIcon(Icon.createWithContentUri("content://null")) + .setStyle(style) + } + .apply(block) + .build() + .also { + setIconPackWithMockIconViews(it) + if (promoted) setPromotedContent(it) + } + +private fun Kosmos.makeOngoingCallStyle(): Notification.CallStyle { + val pendingIntent = + PendingIntent.getBroadcast( + applicationContext, + 0, + Intent("action"), + PendingIntent.FLAG_IMMUTABLE, + ) + val person = Person.Builder().setName("person").build() + return Notification.CallStyle.forOngoingCall(person, pendingIntent) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt index a48b27015c02..fa3702cea5ee 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifPipelineKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock -var Kosmos.notifPipeline by Kosmos.Fixture { mock<NotifPipeline>() } +var Kosmos.notifPipeline by Kosmos.Fixture { mockNotifPipeline } +var Kosmos.mockNotifPipeline by Kosmos.Fixture { mock<NotifPipeline>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt index dc7595f7f2e4..87e0a0f0dda3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.statusbar.data.repository.notificationListenerSettin import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.headsUpNotificationIconInteractor +import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor import com.android.wm.shell.bubbles.bubblesOptional val Kosmos.alwaysOnDisplayNotificationIconsInteractor by Fixture { @@ -47,6 +48,7 @@ val Kosmos.notificationIconsInteractor by Fixture { activeNotificationsInteractor = activeNotificationsInteractor, bubbles = bubblesOptional, headsUpNotificationIconInteractor = headsUpNotificationIconInteractor, + aodPromotedNotificationInteractor = aodPromotedNotificationInteractor, keyguardViewStateRepository = notificationsKeyguardViewStateRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt index 63521de096c9..e55cd0dc16f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.promoted +import android.app.Notification import android.content.applicationContext import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.row.RowImageInflater import com.android.systemui.statusbar.notification.row.shared.skeletonImageTransform var Kosmos.promotedNotificationContentExtractor by @@ -28,3 +31,14 @@ var Kosmos.promotedNotificationContentExtractor by promotedNotificationLogger, ) } + +fun Kosmos.setPromotedContent(entry: NotificationEntry) { + val extractedContent = + promotedNotificationContentExtractor.extractContent( + entry, + Notification.Builder.recoverBuilder(applicationContext, entry.sbn.notification), + RowImageInflater.newInstance(null).useForContentModel(), + ) + entry.promotedNotificationContentModel = + requireNotNull(extractedContent) { "extractContent returned null" } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt index df1c82278bc2..fcd484353011 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt @@ -18,12 +18,11 @@ package com.android.systemui.statusbar.notification.promoted.domain.interactor import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor val Kosmos.aodPromotedNotificationInteractor by Kosmos.Fixture { AODPromotedNotificationInteractor( - activeNotificationsInteractor = activeNotificationsInteractor, + promotedNotificationsInteractor = promotedNotificationsInteractor, dumpManager = dumpManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt new file mode 100644 index 000000000000..093ec10e2642 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/PromotedNotificationsInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.promoted.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.chips.call.domain.interactor.callChipInteractor +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor + +val Kosmos.promotedNotificationsInteractor by + Kosmos.Fixture { + PromotedNotificationsInteractor( + activeNotificationsInteractor = activeNotificationsInteractor, + callChipInteractor = callChipInteractor, + notifChipsInteractor = statusBarNotificationChipsInteractor, + backgroundDispatcher = testDispatcher, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index e445a73b06d0..8b19491bfdf8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -41,6 +41,7 @@ import com.android.systemui.media.controls.util.MediaFeatureFlag import com.android.systemui.media.dialog.MediaOutputDialogManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.settings.UserTracker import com.android.systemui.shared.system.ActivityManagerWrapper import com.android.systemui.shared.system.DevicePolicyManagerWrapper import com.android.systemui.shared.system.PackageManagerWrapper @@ -346,10 +347,15 @@ class ExpandableNotificationRowBuilder( // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be // set, but we do not want to override an existing value that is needed by a specific test. + val userTracker = Mockito.mock(UserTracker::class.java, STUB_ONLY) + whenever(userTracker.userHandle).thenReturn(context.user) + val rowInflaterTask = RowInflaterTask( mFakeSystemClock, Mockito.mock(RowInflaterTaskLogger::class.java, STUB_ONLY), + userTracker, + Mockito.mock(AsyncRowInflater::class.java, STUB_ONLY), ) val row = rowInflaterTask.inflateSynchronously(context, null, entry) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index bc1363ac3d5c..970b87cd368a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -33,7 +33,7 @@ import java.util.Optional val Kosmos.notificationListViewBinder by Fixture { NotificationListViewBinder( - backgroundDispatcher = testDispatcher, + inflationDispatcher = testDispatcher, hiderTracker = displaySwitchNotificationsHiderTracker, configuration = configurationState, falsingManager = falsingManager, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 047bd13f0c27..7a2b7c24252b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -29,7 +29,6 @@ import com.android.systemui.keyguard.ui.viewmodel.aodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.aodToPrimaryBouncerTransitionViewModel -import com.android.systemui.keyguard.ui.viewmodel.dozingToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.dozingToOccludedTransitionViewModel @@ -82,7 +81,6 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, aodToOccludedTransitionViewModel = aodToOccludedTransitionViewModel, aodToPrimaryBouncerTransitionViewModel = aodToPrimaryBouncerTransitionViewModel, - dozingToDreamingTransitionViewModel = dozingToDreamingTransitionViewModel, dozingToGlanceableHubTransitionViewModel = dozingToGlanceableHubTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt new file mode 100644 index 000000000000..81f71e9f7b2f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/data/repository/SecureSettingsForUserRepositoryKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 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.util.settings.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.util.settings.fakeSettings +import com.android.systemui.util.settings.repository.SecureSettingsForUserRepository + +val Kosmos.secureSettingsForUserRepository by + Kosmos.Fixture { + SecureSettingsForUserRepository(fakeSettings, testDispatcher, backgroundCoroutineContext) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt index 703d6ad83eac..a209ec9d0c9c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt @@ -31,3 +31,6 @@ val Kosmos.systemClock by } val Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() } + +val SystemClock.fake + get() = this as FakeSystemClock diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt index 96bc9722635a..8c8d0240f572 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModelKosmos.kt @@ -16,11 +16,11 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel -import android.content.applicationContext import com.android.internal.logging.uiEventLogger import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.domain.interactor.audioSharingInteractor +import com.android.systemui.volume.shared.volumePanelLogger import kotlinx.coroutines.CoroutineScope val Kosmos.audioSharingStreamSliderViewModelFactory by @@ -29,10 +29,10 @@ val Kosmos.audioSharingStreamSliderViewModelFactory by override fun create(coroutineScope: CoroutineScope): AudioSharingStreamSliderViewModel { return AudioSharingStreamSliderViewModel( coroutineScope, - applicationContext, audioSharingInteractor, uiEventLogger, sliderHapticsViewModelFactory, + volumePanelLogger, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt index abd4235143f1..6875619d45fc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.mediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession +import com.android.systemui.volume.shared.volumePanelLogger import kotlinx.coroutines.CoroutineScope val Kosmos.castVolumeSliderViewModelFactory by @@ -36,6 +37,7 @@ val Kosmos.castVolumeSliderViewModelFactory by applicationContext, mediaDeviceSessionInteractor, sliderHapticsViewModelFactory, + volumePanelLogger, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt index aeff86ed89bb..24d2f1f0d901 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperFocalAreaRepository.kt @@ -34,12 +34,15 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository { _wallpaperFocalAreaBounds.asStateFlow() private val _wallpaperFocalAreaTapPosition = MutableStateFlow(PointF(0F, 0F)) - override val wallpaperFocalAreaTapPosition: StateFlow<PointF> = + val wallpaperFocalAreaTapPosition: StateFlow<PointF> = _wallpaperFocalAreaTapPosition.asStateFlow() private val _notificationDefaultTop = MutableStateFlow(0F) override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow() + private val _hasFocalArea = MutableStateFlow(false) + override val hasFocalArea: StateFlow<Boolean> = _hasFocalArea.asStateFlow() + override fun setShortcutAbsoluteTop(top: Float) { _shortcutAbsoluteTop.value = top } @@ -56,7 +59,7 @@ class FakeWallpaperFocalAreaRepository : WallpaperFocalAreaRepository { _wallpaperFocalAreaBounds.value = bounds } - override fun setTapPosition(point: PointF) { - _wallpaperFocalAreaTapPosition.value = point + override fun setTapPosition(tapPosition: PointF) { + _wallpaperFocalAreaTapPosition.value = tapPosition } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt index 8689e04e62dd..66bb803c182d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt @@ -17,6 +17,8 @@ package com.android.systemui.wallpapers.data.repository import android.app.WallpaperInfo +import android.graphics.PointF +import android.graphics.RectF import android.view.View import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -34,9 +36,9 @@ class FakeWallpaperRepository : WallpaperRepository { private val _shouldSendFocalArea = MutableStateFlow(false) override val shouldSendFocalArea: StateFlow<Boolean> = _shouldSendFocalArea.asStateFlow() - fun setShouldSendFocalArea(shouldSendFocalArea: Boolean) { - _shouldSendFocalArea.value = shouldSendFocalArea - } + override fun sendLockScreenLayoutChangeCommand(wallpaperFocalAreaBounds: RectF) {} + + override fun sendTapCommand(tapPosition: PointF) {} fun setWallpaperInfo(wallpaperInfo: WallpaperInfo?) { _wallpaperInfo.value = wallpaperInfo diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt index 7ebec6c3a7b9..1761503b2cc9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt @@ -19,7 +19,6 @@ package com.android.systemui.wallpapers.data.repository import android.content.applicationContext import com.android.app.wallpaperManager import com.android.systemui.broadcast.broadcastDispatcher -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher @@ -34,8 +33,6 @@ val Kosmos.wallpaperRepository by Fixture { bgDispatcher = testDispatcher, broadcastDispatcher = broadcastDispatcher, userRepository = userRepository, - keyguardTransitionInteractor = keyguardTransitionInteractor, - wallpaperFocalAreaRepository = wallpaperFocalAreaRepository, wallpaperManager = wallpaperManager, secureSettings = fakeSettings, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt index 88eb5511160b..eaf55a72be93 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperFocalAreaInteractor.kt @@ -18,20 +18,14 @@ package com.android.systemui.wallpapers.domain.interactor import android.content.applicationContext import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.wallpapers.data.repository.wallpaperFocalAreaRepository -import com.android.systemui.wallpapers.data.repository.wallpaperRepository -val Kosmos.wallpaperFocalAreaInteractor by +var Kosmos.wallpaperFocalAreaInteractor by Kosmos.Fixture { WallpaperFocalAreaInteractor( - applicationScope = applicationCoroutineScope, context = applicationContext, wallpaperFocalAreaRepository = wallpaperFocalAreaRepository, shadeRepository = shadeRepository, - activeNotificationsInteractor = activeNotificationsInteractor, - wallpaperRepository = wallpaperRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt index 7e232c526732..4032503d04c1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/ui/viewmodel/WallpaperFocalAreaViewModel.kt @@ -16,10 +16,14 @@ package com.android.systemui.wallpapers.ui.viewmodel +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor var Kosmos.wallpaperFocalAreaViewModel by Kosmos.Fixture { - WallpaperFocalAreaViewModel(wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor) + WallpaperFocalAreaViewModel( + wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, + ) } diff --git a/ravenwood/runtime-jni/ravenwood_initializer.cpp b/ravenwood/runtime-jni/ravenwood_initializer.cpp index 391c5d56b212..8a35ade649b2 100644 --- a/ravenwood/runtime-jni/ravenwood_initializer.cpp +++ b/ravenwood/runtime-jni/ravenwood_initializer.cpp @@ -26,6 +26,10 @@ #include <fcntl.h> #include <set> +#include <fstream> +#include <iostream> +#include <string> +#include <cstdlib> #include "jni_helper.h" @@ -182,17 +186,82 @@ static jboolean removeSystemProperty(JNIEnv* env, jclass, jstring javaKey) { } } +// Find the PPID of child_pid using /proc/N/stat. The 4th field is the PPID. +// Also returns child_pid's process name (2nd field). +static pid_t getppid_of(pid_t child_pid, std::string& out_process_name) { + if (child_pid < 0) { + return -1; + } + std::string stat_file = "/proc/" + std::to_string(child_pid) + "/stat"; + std::ifstream stat_stream(stat_file); + if (!stat_stream.is_open()) { + ALOGW("Unable to open '%s': %s", stat_file.c_str(), strerror(errno)); + return -1; + } + + std::string field; + int field_count = 0; + while (std::getline(stat_stream, field, ' ')) { + if (++field_count == 4) { + return atoi(field.c_str()); + } + if (field_count == 2) { + out_process_name = field; + } + } + ALOGW("Unexpected format in '%s'", stat_file.c_str()); + return -1; +} + +// Find atest's PID. Climb up the process tree, and find "atest-py3". +static pid_t find_atest_pid() { + auto ret = getpid(); // self (isolation runner process) + + while (ret != -1) { + std::string proc; + ret = getppid_of(ret, proc); + if (proc == "(atest-py3)") { + return ret; + } + } + + return ret; +} + +// If $RAVENWOOD_LOG_OUT is set, redirect stdout/err to this file. +// Originally it was added to allow to monitor log in realtime, with +// RAVENWOOD_LOG_OUT=$(tty) atest... +// +// As a special case, if $RAVENWOOD_LOG_OUT is set to "-", we try to find +// atest's process and send the output to its stdout. It's sort of hacky, but +// this allows shell redirection to work on Ravenwood output too, +// so e.g. `atest ... |tee atest.log` would work on Ravenwood's output. +// (which wouldn't work with `RAVENWOOD_LOG_OUT=$(tty)`). +// +// Otherwise -- if $RAVENWOOD_LOG_OUT isn't set -- atest/tradefed just writes +// the test's output to its own log file. static void maybeRedirectLog() { auto ravenwoodLogOut = getenv("RAVENWOOD_LOG_OUT"); - if (ravenwoodLogOut == NULL) { + if (ravenwoodLogOut == NULL || *ravenwoodLogOut == '\0') { return; } - ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to %s", ravenwoodLogOut); + std::string path; + if (strcmp("-", ravenwoodLogOut) == 0) { + pid_t ppid = find_atest_pid(); + if (ppid < 0) { + ALOGI("RAVENWOOD_LOG_OUT set to '-', but unable to find atest's PID"); + return; + } + path = std::format("/proc/{}/fd/1", ppid); + } else { + path = ravenwoodLogOut; + } + ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to '%s'", path.c_str()); // Redirect stdin / stdout to /dev/tty. - int ttyFd = open(ravenwoodLogOut, O_WRONLY | O_APPEND); + int ttyFd = open(path.c_str(), O_WRONLY | O_APPEND); if (ttyFd == -1) { - ALOGW("$RAVENWOOD_LOG_OUT is set to %s, but failed to open: %s ", ravenwoodLogOut, + ALOGW("$RAVENWOOD_LOG_OUT is set, but failed to open '%s': %s ", path.c_str(), strerror(errno)); return; } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 529a564ea607..bb0eacb5afa7 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -145,6 +145,13 @@ flag { } flag { + name: "enable_magnification_follows_mouse_with_pointer_motion_filter" + namespace: "accessibility" + description: "Whether to enable mouse following using pointer motion filter" + bug: "361817142" +} + +flag { name: "enable_magnification_keyboard_control" namespace: "accessibility" description: "Whether to enable keyboard control for magnification" diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java index 89c9d690a82c..700a1624f7d4 100644 --- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java +++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java @@ -34,6 +34,8 @@ import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUC import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; import android.app.ActivityOptions; import android.app.AppOpsManager; import android.app.admin.DevicePolicyManagerInternal; @@ -69,7 +71,6 @@ import android.provider.Settings; import android.util.Log; import android.util.Slog; import android.view.IWindowManager; -import android.window.ScreenCapture; import android.window.ScreenCapture.ScreenshotHardwareBuffer; import com.android.internal.R; @@ -86,7 +87,6 @@ import java.io.FileDescriptor; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.Set; public class ContextualSearchManagerService extends SystemService { private static final String TAG = ContextualSearchManagerService.class.getSimpleName(); @@ -95,9 +95,20 @@ public class ContextualSearchManagerService extends SystemService { private static final int MSG_INVALIDATE_TOKEN = 1; private static final int MAX_TOKEN_VALID_DURATION_MS = 1_000 * 60 * 10; // 10 minutes + /** + * Below are internal entrypoints not supported by the + * {@link ContextualSearchManager#startContextualSearch(int entrypoint)} method. + * + * <p>These values should be negative to avoid conflicting with the system entrypoints. + */ + + /** Entrypoint to be used when a foreground app invokes Contextual Search. */ + private static final int INTERNAL_ENTRYPOINT_APP = -1; + private static final boolean DEBUG = false; private final Context mContext; + private final ActivityManagerInternal mActivityManagerInternal; private final ActivityTaskManagerInternal mAtmInternal; private final PackageManagerInternal mPackageManager; private final WindowManagerInternal mWmInternal; @@ -162,6 +173,8 @@ public class ContextualSearchManagerService extends SystemService { super(context); if (DEBUG) Log.d(TAG, "ContextualSearchManagerService created"); mContext = context; + mActivityManagerInternal = Objects.requireNonNull( + LocalServices.getService(ActivityManagerInternal.class)); mAtmInternal = Objects.requireNonNull( LocalServices.getService(ActivityTaskManagerInternal.class)); mPackageManager = LocalServices.getService(PackageManagerInternal.class); @@ -391,6 +404,20 @@ public class ContextualSearchManagerService extends SystemService { } } + private void enforceForegroundApp(@NonNull final String func) { + final int callingUid = Binder.getCallingUid(); + final String callingPackage = mPackageManager.getNameForUid(Binder.getCallingUid()); + if (mActivityManagerInternal.getUidProcessState(callingUid) + > ActivityManager.PROCESS_STATE_TOP) { + // The calling process must be displaying an activity in foreground to + // trigger contextual search. + String msg = "Permission Denial: Cannot call " + func + " from pid=" + + Binder.getCallingPid() + ", uid=" + callingUid + + ", package=" + callingPackage + " without a foreground activity."; + throw new SecurityException(msg); + } + } + private void enforceOverridingPermission(@NonNull final String func) { if (!(Binder.getCallingUid() == Process.SHELL_UID || Binder.getCallingUid() == Process.ROOT_UID @@ -448,29 +475,43 @@ public class ContextualSearchManagerService extends SystemService { } @Override + public void startContextualSearchForForegroundApp() { + synchronized (this) { + if (DEBUG) { + Log.d(TAG, "Starting contextual search from: " + + mPackageManager.getNameForUid(Binder.getCallingUid())); + } + enforceForegroundApp("startContextualSearchForForegroundApp"); + startContextualSearchInternal(INTERNAL_ENTRYPOINT_APP); + } + } + + @Override public void startContextualSearch(int entrypoint) { synchronized (this) { if (DEBUG) Log.d(TAG, "startContextualSearch entrypoint: " + entrypoint); enforcePermission("startContextualSearch"); - final int callingUserId = Binder.getCallingUserHandle().getIdentifier(); - - mAssistDataRequester.cancel(); - // Creates a new CallbackToken at mToken and an expiration handler. - issueToken(); - // We get the launch intent with the system server's identity because the system - // server has READ_FRAME_BUFFER permission to get the screenshot and because only - // the system server can invoke non-exported activities. - Binder.withCleanCallingIdentity(() -> { - Intent launchIntent = - getContextualSearchIntent(entrypoint, callingUserId, mToken); - if (launchIntent != null) { - int result = invokeContextualSearchIntent(launchIntent, callingUserId); - if (DEBUG) Log.d(TAG, "Launch result: " + result); - } - }); + startContextualSearchInternal(entrypoint); } } + private void startContextualSearchInternal(int entrypoint) { + final int callingUserId = Binder.getCallingUserHandle().getIdentifier(); + mAssistDataRequester.cancel(); + // Creates a new CallbackToken at mToken and an expiration handler. + issueToken(); + // We get the launch intent with the system server's identity because the system + // server has READ_FRAME_BUFFER permission to get the screenshot and because only + // the system server can invoke non-exported activities. + Binder.withCleanCallingIdentity(() -> { + Intent launchIntent = getContextualSearchIntent(entrypoint, callingUserId, mToken); + if (launchIntent != null) { + int result = invokeContextualSearchIntent(launchIntent, callingUserId); + if (DEBUG) Log.d(TAG, "Launch result: " + result); + } + }); + } + @Override public void getContextualSearchState( @NonNull IBinder token, diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 4976a63b016b..8eda17698b9b 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -18,7 +18,6 @@ package com.android.server; import static android.app.Flags.enableCurrentModeTypeBinderCache; import static android.app.Flags.enableNightModeBinderCache; -import static android.app.Flags.modesApi; import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE; import static android.app.UiModeManager.DEFAULT_PRIORITY; import static android.app.UiModeManager.FORCE_INVERT_TYPE_DARK; @@ -2208,14 +2207,12 @@ final class UiModeManagerService extends SystemService { appliedOverrides = true; } - if (modesApi()) { - // Computes final night mode values based on Attention Mode. - mComputedNightMode = switch (mAttentionModeThemeOverlay) { - case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true; - case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false; - default -> newComputedValue; // case OFF - }; - } + // Computes final night mode values based on Attention Mode. + mComputedNightMode = switch (mAttentionModeThemeOverlay) { + case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true; + case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false; + default -> newComputedValue; // case OFF + }; if (appliedOverrides) { return; diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index 961022b7231b..517279bd7527 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -54,15 +54,21 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ProcessMap; import com.android.internal.os.Clock; import com.android.internal.os.MonotonicClock; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; import com.android.server.IoThread; import com.android.server.ServiceThread; import com.android.server.SystemServiceManager; import com.android.server.wm.WindowProcessController; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -1006,6 +1012,12 @@ public final class AppStartInfoTracker { throws IOException, WireTypeMismatchException, ClassNotFoundException { long token = proto.start(fieldId); String pkgName = ""; + + // Create objects for reuse. + ByteArrayInputStream byteArrayInputStream = null; + ObjectInputStream objectInputStream = null; + TypedXmlPullParser typedXmlPullParser = null; + for (int next = proto.nextField(); next != ProtoInputStream.NO_MORE_FIELDS; next = proto.nextField()) { @@ -1017,7 +1029,7 @@ public final class AppStartInfoTracker { AppStartInfoContainer container = new AppStartInfoContainer(mAppStartInfoHistoryListSize); int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS, - pkgName); + pkgName, byteArrayInputStream, objectInputStream, typedXmlPullParser); // If the isolated process flag is enabled and the uid is that of an isolated // process, then break early so that the container will not be added to mData. @@ -1052,6 +1064,12 @@ public final class AppStartInfoTracker { out = af.startWrite(); ProtoOutputStream proto = new ProtoOutputStream(out); proto.write(AppsStartInfoProto.LAST_UPDATE_TIMESTAMP, now); + + // Create objects for reuse. + ByteArrayOutputStream byteArrayOutputStream = null; + ObjectOutputStream objectOutputStream = null; + TypedXmlSerializer typedXmlSerializer = null; + synchronized (mLock) { succeeded = forEachPackageLocked( (packageName, records) -> { @@ -1060,8 +1078,9 @@ public final class AppStartInfoTracker { int uidArraySize = records.size(); for (int j = 0; j < uidArraySize; j++) { try { - records.valueAt(j) - .writeToProto(proto, AppsStartInfoProto.Package.USERS); + records.valueAt(j).writeToProto(proto, + AppsStartInfoProto.Package.USERS, byteArrayOutputStream, + objectOutputStream, typedXmlSerializer); } catch (IOException e) { Slog.w(TAG, "Unable to write app start info into persistent" + "storage: " + e); @@ -1414,19 +1433,23 @@ public final class AppStartInfoTracker { } @GuardedBy("mLock") - void writeToProto(ProtoOutputStream proto, long fieldId) throws IOException { + void writeToProto(ProtoOutputStream proto, long fieldId, + ByteArrayOutputStream byteArrayOutputStream, ObjectOutputStream objectOutputStream, + TypedXmlSerializer typedXmlSerializer) throws IOException { long token = proto.start(fieldId); proto.write(AppsStartInfoProto.Package.User.UID, mUid); int size = mInfos.size(); for (int i = 0; i < size; i++) { - mInfos.get(i) - .writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO); + mInfos.get(i).writeToProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO, + byteArrayOutputStream, objectOutputStream, typedXmlSerializer); } proto.write(AppsStartInfoProto.Package.User.MONITORING_ENABLED, mMonitoringModeEnabled); proto.end(token); } - int readFromProto(ProtoInputStream proto, long fieldId, String packageName) + int readFromProto(ProtoInputStream proto, long fieldId, String packageName, + ByteArrayInputStream byteArrayInputStream, ObjectInputStream objectInputStream, + TypedXmlPullParser typedXmlPullParser) throws IOException, WireTypeMismatchException, ClassNotFoundException { long token = proto.start(fieldId); for (int next = proto.nextField(); @@ -1440,7 +1463,8 @@ public final class AppStartInfoTracker { // Create record with monotonic time 0 in case the persisted record does not // have a create time. ApplicationStartInfo info = new ApplicationStartInfo(0); - info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO); + info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO, + byteArrayInputStream, objectInputStream, typedXmlPullParser); info.setPackageName(packageName); mInfos.add(info); break; diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java index 30c2a82296ca..604cb30294a9 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -418,7 +418,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { evictedEvents.addAll(mCache); mCache.clear(); } - mSqliteWriteHandler.obtainMessage(WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents); + Message msg = mSqliteWriteHandler.obtainMessage( + WRITE_CACHE_EVICTED_OP_EVENTS, evictedEvents); + mSqliteWriteHandler.sendMessage(msg); } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 6d6e1fb6bfb3..ef80d59993e9 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -18,6 +18,7 @@ package com.android.server.audio; import static android.media.audio.Flags.scoManagedByAudio; import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_HEADSET; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_BLE_SPEAKER; import static com.android.server.audio.AudioService.BT_COMM_DEVICE_ACTIVE_SCO; @@ -290,8 +291,8 @@ public class AudioDeviceBroker { } @GuardedBy("mDeviceStateLock") - /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice) { - mBtHelper.onSetBtScoActiveDevice(btDevice); + /*package*/ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) { + mBtHelper.onSetBtScoActiveDevice(btDevice, deviceSwitch); } /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) { @@ -941,6 +942,7 @@ public class AudioDeviceBroker { final @NonNull String mEventSource; final int mAudioSystemDevice; final int mMusicDevice; + final boolean mIsDeviceSwitch; BtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device, int state, int audioDevice, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) { @@ -953,6 +955,8 @@ public class AudioDeviceBroker { mEventSource = d.mEventSource; mAudioSystemDevice = audioDevice; mMusicDevice = AudioSystem.DEVICE_NONE; + mIsDeviceSwitch = optimizeBtDeviceSwitch() + && d.mNewDevice != null && d.mPreviousDevice != null; } // constructor used by AudioDeviceBroker to search similar message @@ -966,6 +970,7 @@ public class AudioDeviceBroker { mSupprNoisy = false; mVolume = -1; mIsLeOutput = false; + mIsDeviceSwitch = false; } // constructor used by AudioDeviceInventory when config change failed @@ -980,6 +985,7 @@ public class AudioDeviceBroker { mSupprNoisy = false; mVolume = -1; mIsLeOutput = false; + mIsDeviceSwitch = false; } BtDeviceInfo(@NonNull BtDeviceInfo src, int state) { @@ -992,6 +998,7 @@ public class AudioDeviceBroker { mEventSource = src.mEventSource; mAudioSystemDevice = src.mAudioSystemDevice; mMusicDevice = src.mMusicDevice; + mIsDeviceSwitch = false; } // redefine equality op so we can match messages intended for this device @@ -1026,7 +1033,8 @@ public class AudioDeviceBroker { + " isLeOutput=" + mIsLeOutput + " eventSource=" + mEventSource + " audioSystemDevice=" + mAudioSystemDevice - + " musicDevice=" + mMusicDevice; + + " musicDevice=" + mMusicDevice + + " isDeviceSwitch=" + mIsDeviceSwitch; } } @@ -1196,6 +1204,8 @@ public class AudioDeviceBroker { AudioSystem.setParameters("A2dpSuspended=true"); AudioSystem.setParameters("LeAudioSuspended=true"); AudioSystem.setParameters("BT_SCO=on"); + mBluetoothA2dpSuspendedApplied = true; + mBluetoothLeSuspendedApplied = true; } else { AudioSystem.setParameters("BT_SCO=off"); if (mBluetoothA2dpSuspendedApplied) { @@ -1680,10 +1690,11 @@ public class AudioDeviceBroker { } /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes, - boolean connect, @Nullable BluetoothDevice btDevice) { + boolean connect, @Nullable BluetoothDevice btDevice, + boolean deviceSwitch) { synchronized (mDeviceStateLock) { return mDeviceInventory.handleDeviceConnection( - attributes, connect, false /*for test*/, btDevice); + attributes, connect, false /*for test*/, btDevice, deviceSwitch); } } @@ -1776,6 +1787,18 @@ public class AudioDeviceBroker { pw.println("\n" + prefix + "mScoManagedByAudio: " + mScoManagedByAudio); + pw.println("\n" + prefix + "Bluetooth SCO on" + + ", requested: " + mBluetoothScoOn + + ", applied: " + mBluetoothScoOnApplied); + pw.println("\n" + prefix + "Bluetooth A2DP suspended" + + ", requested ext: " + mBluetoothA2dpSuspendedExt + + ", requested int: " + mBluetoothA2dpSuspendedInt + + ", applied " + mBluetoothA2dpSuspendedApplied); + pw.println("\n" + prefix + "Bluetooth LE Audio suspended" + + ", requested ext: " + mBluetoothLeSuspendedExt + + ", requested int: " + mBluetoothLeSuspendedInt + + ", applied " + mBluetoothLeSuspendedApplied); + mBtHelper.dump(pw, prefix); } @@ -1930,10 +1953,12 @@ public class AudioDeviceBroker { || btInfo.mIsLeOutput) ? mAudioService.getBluetoothContextualVolumeStream() : AudioSystem.STREAM_DEFAULT); - if (btInfo.mProfile == BluetoothProfile.LE_AUDIO + if ((btInfo.mProfile == BluetoothProfile.LE_AUDIO || btInfo.mProfile == BluetoothProfile.HEARING_AID || (mScoManagedByAudio - && btInfo.mProfile == BluetoothProfile.HEADSET)) { + && btInfo.mProfile == BluetoothProfile.HEADSET)) + && (btInfo.mState == BluetoothProfile.STATE_CONNECTED + || !btInfo.mIsDeviceSwitch)) { onUpdateCommunicationRouteClient( bluetoothScoRequestOwnerAttributionSource(), "setBluetoothActiveDevice"); diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index ef10793fd955..ae91934e7498 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -799,7 +799,7 @@ public class AudioDeviceInventory { di.mDeviceAddress, di.mDeviceName), AudioSystem.DEVICE_STATE_AVAILABLE, - di.mDeviceCodecFormat); + di.mDeviceCodecFormat, false /*deviceSwitch*/); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { failedReconnectionDeviceList.add(di); } @@ -811,7 +811,7 @@ public class AudioDeviceInventory { EventLogger.Event.ALOGE, TAG); mConnectedDevices.remove(di.getKey(), di); if (AudioSystem.isBluetoothScoDevice(di.mDeviceType)) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/); } } } @@ -851,7 +851,8 @@ public class AudioDeviceInventory { Log.d(TAG, "onSetBtActiveDevice" + " btDevice=" + btInfo.mDevice + " profile=" + BluetoothProfile.getProfileName(btInfo.mProfile) - + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState)); + + " state=" + BluetoothProfile.getConnectionStateName(btInfo.mState) + + " isDeviceSwitch=" + btInfo.mIsDeviceSwitch); } String address = btInfo.mDevice.getAddress(); if (!BluetoothAdapter.checkBluetoothAddress(address)) { @@ -897,7 +898,8 @@ public class AudioDeviceInventory { break; case BluetoothProfile.A2DP: if (switchToUnavailable) { - makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat); + makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat, + btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { // device is not already connected if (btInfo.mVolume != -1) { @@ -911,7 +913,7 @@ public class AudioDeviceInventory { break; case BluetoothProfile.HEARING_AID: if (switchToUnavailable) { - makeHearingAidDeviceUnavailable(address); + makeHearingAidDeviceUnavailable(address, btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { makeHearingAidDeviceAvailable(address, BtHelper.getName(btInfo.mDevice), streamType, "onSetBtActiveDevice"); @@ -921,7 +923,8 @@ public class AudioDeviceInventory { case BluetoothProfile.LE_AUDIO_BROADCAST: if (switchToUnavailable) { makeLeAudioDeviceUnavailableNow(address, - btInfo.mAudioSystemDevice, di.mDeviceCodecFormat); + btInfo.mAudioSystemDevice, di.mDeviceCodecFormat, + btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { makeLeAudioDeviceAvailable( btInfo, streamType, codec, "onSetBtActiveDevice"); @@ -930,9 +933,10 @@ public class AudioDeviceInventory { case BluetoothProfile.HEADSET: if (mDeviceBroker.isScoManagedByAudio()) { if (switchToUnavailable) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, btInfo.mIsDeviceSwitch); } else if (switchToAvailable) { - mDeviceBroker.onSetBtScoActiveDevice(btInfo.mDevice); + mDeviceBroker.onSetBtScoActiveDevice( + btInfo.mDevice, false /*deviceSwitch*/); } } break; @@ -1053,19 +1057,19 @@ public class AudioDeviceInventory { /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) { synchronized (mDevicesLock) { - makeA2dpDeviceUnavailableNow(address, a2dpCodec); + makeA2dpDeviceUnavailableNow(address, a2dpCodec, false /*deviceSwitch*/); } } /*package*/ void onMakeLeAudioDeviceUnavailableNow(String address, int device, int codec) { synchronized (mDevicesLock) { - makeLeAudioDeviceUnavailableNow(address, device, codec); + makeLeAudioDeviceUnavailableNow(address, device, codec, false /*deviceSwitch*/); } } /*package*/ void onMakeHearingAidDeviceUnavailableNow(String address) { synchronized (mDevicesLock) { - makeHearingAidDeviceUnavailable(address); + makeHearingAidDeviceUnavailable(address, false /*deviceSwitch*/); } } @@ -1180,7 +1184,8 @@ public class AudioDeviceInventory { } if (!handleDeviceConnection(wdcs.mAttributes, - wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, null)) { + wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED, wdcs.mForTest, + null, false /*deviceSwitch*/)) { // change of connection state failed, bailout mmi.set(MediaMetrics.Property.EARLY_RETURN, "change of connection state failed") .record(); @@ -1788,14 +1793,15 @@ public class AudioDeviceInventory { */ /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes, boolean connect, boolean isForTesting, - @Nullable BluetoothDevice btDevice) { + @Nullable BluetoothDevice btDevice, + boolean deviceSwitch) { int device = attributes.getInternalType(); String address = attributes.getAddress(); String deviceName = attributes.getName(); if (AudioService.DEBUG_DEVICES) { Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:" + Integer.toHexString(device) + " address:" + address - + " name:" + deviceName + ")"); + + " name:" + deviceName + ", deviceSwitch: " + deviceSwitch + ")"); } MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "handleDeviceConnection") .set(MediaMetrics.Property.ADDRESS, address) @@ -1829,7 +1835,8 @@ public class AudioDeviceInventory { res = AudioSystem.AUDIO_STATUS_OK; } else { res = mAudioSystem.setDeviceConnectionState(attributes, - AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT, + false /*deviceSwitch*/); } if (res != AudioSystem.AUDIO_STATUS_OK) { final String reason = "not connecting device 0x" + Integer.toHexString(device) @@ -1856,7 +1863,8 @@ public class AudioDeviceInventory { status = true; } else if (!connect && isConnected) { mAudioSystem.setDeviceConnectionState(attributes, - AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT, + deviceSwitch); // always remove even if disconnection failed mConnectedDevices.remove(deviceKey); mDeviceBroker.postCheckCommunicationDeviceRemoval(attributes); @@ -2030,7 +2038,7 @@ public class AudioDeviceInventory { } } if (disconnect) { - mDeviceBroker.onSetBtScoActiveDevice(null); + mDeviceBroker.onSetBtScoActiveDevice(null, false /*deviceSwitch*/); } } @@ -2068,7 +2076,8 @@ public class AudioDeviceInventory { || info.mProfile == BluetoothProfile.LE_AUDIO_BROADCAST) && info.mIsLeOutput) || info.mProfile == BluetoothProfile.HEARING_AID - || info.mProfile == BluetoothProfile.A2DP)) { + || info.mProfile == BluetoothProfile.A2DP) + && !info.mIsDeviceSwitch) { @AudioService.ConnectionState int asState = (info.mState == BluetoothProfile.STATE_CONNECTED) ? AudioService.CONNECTION_STATE_CONNECTED @@ -2124,7 +2133,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_AVAILABLE, codec); + AudioSystem.DEVICE_STATE_AVAILABLE, codec, false); // TODO: log in MediaMetrics once distinction between connection failure and // double connection is made. @@ -2362,7 +2371,7 @@ public class AudioDeviceInventory { } @GuardedBy("mDevicesLock") - private void makeA2dpDeviceUnavailableNow(String address, int codec) { + private void makeA2dpDeviceUnavailableNow(String address, int codec, boolean deviceSwitch) { MediaMetrics.Item mmi = new MediaMetrics.Item(mMetricsId + "a2dp." + address) .set(MediaMetrics.Property.ENCODING, AudioSystem.audioFormatToString(codec)) .set(MediaMetrics.Property.EVENT, "makeA2dpDeviceUnavailableNow"); @@ -2393,7 +2402,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_UNAVAILABLE, codec); + AudioSystem.DEVICE_STATE_UNAVAILABLE, codec, deviceSwitch); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( @@ -2404,7 +2413,8 @@ public class AudioDeviceInventory { } else { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) - + " made unavailable")).printSlog(EventLogger.Event.ALOGI, TAG)); + + " made unavailable, deviceSwitch" + deviceSwitch)) + .printSlog(EventLogger.Event.ALOGI, TAG)); } mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); @@ -2440,7 +2450,7 @@ public class AudioDeviceInventory { final int res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address), AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make available A2DP source device addr=" @@ -2465,7 +2475,7 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); // always remove regardless of the result mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address)); @@ -2485,7 +2495,7 @@ public class AudioDeviceInventory { DEVICE_OUT_HEARING_AID, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, false); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available HearingAid addr=" + address @@ -2515,12 +2525,12 @@ public class AudioDeviceInventory { } @GuardedBy("mDevicesLock") - private void makeHearingAidDeviceUnavailable(String address) { + private void makeHearingAidDeviceUnavailable(String address, boolean deviceSwitch) { AudioDeviceAttributes ada = new AudioDeviceAttributes( DEVICE_OUT_HEARING_AID, address); mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT); + AudioSystem.AUDIO_FORMAT_DEFAULT, deviceSwitch); // always remove regardless of return code mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); @@ -2622,7 +2632,7 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes(device, address, name); final int res = mAudioSystem.setDeviceConnectionState(ada, - AudioSystem.DEVICE_STATE_AVAILABLE, codec); + AudioSystem.DEVICE_STATE_AVAILABLE, codec, false /*deviceSwitch*/); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available LE Audio device addr=" + address @@ -2669,13 +2679,13 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeLeAudioDeviceUnavailableNow(String address, int device, - @AudioSystem.AudioFormatNativeEnumForBtCodec int codec) { + @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, boolean deviceSwitch) { AudioDeviceAttributes ada = null; if (device != AudioSystem.DEVICE_NONE) { ada = new AudioDeviceAttributes(device, address); final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, - codec); + codec, deviceSwitch); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( @@ -2685,7 +2695,8 @@ public class AudioDeviceInventory { } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) - + " made unavailable").printSlog(EventLogger.Event.ALOGI, TAG)); + + " made unavailable, deviceSwitch" + deviceSwitch) + .printSlog(EventLogger.Event.ALOGI, TAG)); } mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address)); } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 86871ea45d13..057d1274d47d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -63,6 +63,7 @@ import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.deferWearPermissionUpdates; import static com.android.media.audio.Flags.equalScoLeaVcIndexRange; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; import static com.android.media.audio.Flags.replaceStreamBtSco; import static com.android.media.audio.Flags.ringMyCar; import static com.android.media.audio.Flags.ringerModeAffectsAlarm; @@ -4990,6 +4991,8 @@ public class AudioService extends IAudioService.Stub + cacheGetStreamMinMaxVolume()); pw.println("\tandroid.media.audio.Flags.cacheGetStreamVolume:" + cacheGetStreamVolume()); + pw.println("\tcom.android.media.audio.optimizeBtDeviceSwitch:" + + optimizeBtDeviceSwitch()); } private void dumpAudioMode(PrintWriter pw) { diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index e86c34cab88a..a6267c156fb3 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -367,9 +367,9 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, * @return */ public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, - int codecFormat) { + int codecFormat, boolean deviceSwitch) { invalidateRoutingCache(); - return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat); + return AudioSystem.setDeviceConnectionState(attributes, state, codecFormat, deviceSwitch); } /** diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 922116999bc7..844e3524384d 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -26,6 +26,8 @@ import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN; import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH; +import static com.android.media.audio.Flags.optimizeBtDeviceSwitch; + import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothA2dp; @@ -393,8 +395,11 @@ public class BtHelper { + "received with null profile proxy for device: " + btDevice)).printLog(TAG)); return; + } - onSetBtScoActiveDevice(btDevice); + boolean deviceSwitch = optimizeBtDeviceSwitch() + && btDevice != null && mBluetoothHeadsetDevice != null; + onSetBtScoActiveDevice(btDevice, deviceSwitch); } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); onScoAudioStateChanged(btState); @@ -814,7 +819,7 @@ public class BtHelper { if (device == null) { continue; } - onSetBtScoActiveDevice(device); + onSetBtScoActiveDevice(device, false /*deviceSwitch*/); } } else { Log.e(TAG, "onHeadsetProfileConnected: Null BluetoothAdapter"); @@ -907,7 +912,8 @@ public class BtHelper { } @GuardedBy("mDeviceBroker.mDeviceStateLock") - private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { + private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive, + boolean deviceSwitch) { if (btDevice == null) { return true; } @@ -919,12 +925,12 @@ public class BtHelper { if (isActive) { audioDevice = btHeadsetDeviceToAudioDevice(btDevice); result = mDeviceBroker.handleDeviceConnection( - audioDevice, true /*connect*/, btDevice); + audioDevice, true /*connect*/, btDevice, false /*deviceSwitch*/); } else { AudioDeviceAttributes ada = mResolvedScoAudioDevices.get(btDevice); if (ada != null) { result = mDeviceBroker.handleDeviceConnection( - ada, false /*connect*/, btDevice); + ada, false /*connect*/, btDevice, deviceSwitch); } else { // Disconnect all possible audio device types if the disconnected device type is // unknown @@ -935,7 +941,8 @@ public class BtHelper { }; for (int outDeviceType : outDeviceTypes) { result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( - outDeviceType, address, name), false /*connect*/, btDevice); + outDeviceType, address, name), false /*connect*/, btDevice, + deviceSwitch); } } } @@ -944,7 +951,7 @@ public class BtHelper { // handleDeviceConnection() && result to make sure the method get executed result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes( inDevice, address, name), - isActive, btDevice) && result; + isActive, btDevice, deviceSwitch) && result; if (result) { if (isActive) { mResolvedScoAudioDevices.put(btDevice, audioDevice); @@ -961,18 +968,18 @@ public class BtHelper { } @GuardedBy("mDeviceBroker.mDeviceStateLock") - /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice) { + /*package */ void onSetBtScoActiveDevice(BluetoothDevice btDevice, boolean deviceSwitch) { Log.i(TAG, "onSetBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice) - + " -> " + getAnonymizedAddress(btDevice)); + + " -> " + getAnonymizedAddress(btDevice) + ", deviceSwitch: " + deviceSwitch); final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice; if (Objects.equals(btDevice, previousActiveDevice)) { return; } - if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) { + if (!handleBtScoActiveDeviceChange(previousActiveDevice, false, deviceSwitch)) { Log.w(TAG, "onSetBtScoActiveDevice() failed to remove previous device " + getAnonymizedAddress(previousActiveDevice)); } - if (!handleBtScoActiveDeviceChange(btDevice, true)) { + if (!handleBtScoActiveDeviceChange(btDevice, true, false /*deviceSwitch*/)) { Log.e(TAG, "onSetBtScoActiveDevice() failed to add new device " + getAnonymizedAddress(btDevice)); // set mBluetoothHeadsetDevice to null when failing to add new device diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index b11267ef8634..79523bd02404 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -69,6 +69,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final String SYSTEM_GENDER_HELPER = "system_gender"; private static final String DISPLAY_HELPER = "display"; private static final String INPUT_HELPER = "input"; + private static final String WEAR_BACKUP_HELPER = "wear"; // These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME // are also used in the full-backup file format, so must not change unless steps are @@ -113,7 +114,7 @@ public class SystemBackupAgent extends BackupAgentHelper { private static final Set<String> sEligibleHelpersForNonSystemUser = SetUtils.union(sEligibleHelpersForProfileUser, Sets.newArraySet(ACCOUNT_MANAGER_HELPER, USAGE_STATS_HELPER, PREFERRED_HELPER, - SHORTCUT_MANAGER_HELPER, INPUT_HELPER)); + SHORTCUT_MANAGER_HELPER, INPUT_HELPER, WEAR_BACKUP_HELPER)); private int mUserId = UserHandle.USER_SYSTEM; private boolean mIsProfileUser = false; @@ -153,6 +154,11 @@ public class SystemBackupAgent extends BackupAgentHelper { if (com.android.hardware.input.Flags.enableBackupAndRestoreForInputGestures()) { addHelperIfEligibleForUser(INPUT_HELPER, new InputBackupHelper(mUserId)); } + + // Add Wear helper only if the device is a watch + if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { + addHelperIfEligibleForUser(WEAR_BACKUP_HELPER, new WearBackupHelper()); + } } @Override diff --git a/services/core/java/com/android/server/backup/WearBackupHelper.java b/services/core/java/com/android/server/backup/WearBackupHelper.java new file mode 100644 index 000000000000..27416b3eb2a6 --- /dev/null +++ b/services/core/java/com/android/server/backup/WearBackupHelper.java @@ -0,0 +1,49 @@ +/* + * Copyright 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import android.annotation.Nullable; +import android.app.backup.BlobBackupHelper; + +import com.android.server.LocalServices; + +/** A {@link android.app.backup.BlobBackupHelper} for Wear */ +public class WearBackupHelper extends BlobBackupHelper { + + private static final int BLOB_VERSION = 1; + private static final String KEY_WEAR_BACKUP = "wear"; + @Nullable private final WearBackupInternal mWearBackupInternal; + + public WearBackupHelper() { + super(BLOB_VERSION, KEY_WEAR_BACKUP); + mWearBackupInternal = LocalServices.getService(WearBackupInternal.class); + } + + @Override + protected byte[] getBackupPayload(String key) { + return KEY_WEAR_BACKUP.equals(key) && mWearBackupInternal != null + ? mWearBackupInternal.getBackupPayload(getLogger()) + : null; + } + + @Override + protected void applyRestoredPayload(String key, byte[] payload) { + if (KEY_WEAR_BACKUP.equals(key) && mWearBackupInternal != null) { + mWearBackupInternal.applyRestoredPayload(payload); + } + } +} diff --git a/services/core/java/com/android/server/backup/WearBackupInternal.java b/services/core/java/com/android/server/backup/WearBackupInternal.java new file mode 100644 index 000000000000..7b4847b51df6 --- /dev/null +++ b/services/core/java/com/android/server/backup/WearBackupInternal.java @@ -0,0 +1,32 @@ +/* + * Copyright 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import android.app.backup.BackupRestoreEventLogger; + +import com.android.internal.annotations.Keep; + +/** A local service internal for Wear OS handle backup/restore */ +@Keep +public interface WearBackupInternal { + + /** Gets the backup payload */ + byte[] getBackupPayload(BackupRestoreEventLogger logger); + + /** Applies the restored payload */ + void applyRestoredPayload(byte[] payload); +} diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index b6a3f4041b13..d4bb1d52c111 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2587,6 +2587,11 @@ public final class DisplayManagerService extends SystemService { sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED); } + private void handleLogicalDisplayCommittedStateChangedLocked(@NonNull LogicalDisplay display) { + sendDisplayEventIfEnabledLocked(display, + DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED); + } + private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) { mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked() .mDisplayDeviceConfig); @@ -2609,7 +2614,8 @@ public final class DisplayManagerService extends SystemService { // Blank or unblank the display immediately to match the state requested // by the display power controller (if known). DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); - if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { + if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 + || android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device); if (display == null) { return null; @@ -4165,6 +4171,9 @@ public final class DisplayManagerService extends SystemService { case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_STATE_CHANGED: handleLogicalDisplayStateChangedLocked(display); break; + case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED: + handleLogicalDisplayCommittedStateChangedLocked(display); + break; } } @@ -4419,6 +4428,9 @@ public final class DisplayManagerService extends SystemService { case DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED: return (mask & DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_STATE) != 0; + case DisplayManagerGlobal.EVENT_DISPLAY_COMMITTED_STATE_CHANGED: + return (mask & DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_COMMITTED_STATE_CHANGED) != 0; default: // This should never happen. Slog.e(TAG, "Unknown display event " + event); @@ -5563,7 +5575,9 @@ public final class DisplayManagerService extends SystemService { final DisplayDevice displayDevice = mLogicalDisplayMapper.getDisplayLocked( id).getPrimaryDisplayDeviceLocked(); final int flags = displayDevice.getDisplayDeviceInfoLocked().flags; - if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { + if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0 + || android.companion.virtualdevice.flags.Flags + .correctVirtualDisplayPowerState()) { final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(id); if (displayPowerController != null) { diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 02db051dff57..872f33484951 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -91,6 +91,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { public static final int LOGICAL_DISPLAY_EVENT_DISCONNECTED = 1 << 8; public static final int LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED = 1 << 9; public static final int LOGICAL_DISPLAY_EVENT_STATE_CHANGED = 1 << 10; + public static final int LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED = 1 << 11; + public static final int DISPLAY_GROUP_EVENT_ADDED = 1; public static final int DISPLAY_GROUP_EVENT_CHANGED = 2; @@ -810,7 +812,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { int logicalDisplayEventMask = mLogicalDisplaysToUpdate .get(displayId, LOGICAL_DISPLAY_EVENT_BASE); boolean hasBasicInfoChanged = - !mTempDisplayInfo.equals(newDisplayInfo, /* compareRefreshRate */ false); + !mTempDisplayInfo.equals(newDisplayInfo, /* compareOnlyBasicChanges */ true); // The display is no longer valid and needs to be removed. if (!display.isValidLocked()) { // Remove from group @@ -930,6 +932,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED); + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); @@ -961,6 +964,11 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { && mTempDisplayInfo.state != newDisplayInfo.state) { mask |= LOGICAL_DISPLAY_EVENT_STATE_CHANGED; } + + if (mFlags.isCommittedStateSeparateEventEnabled() + && mTempDisplayInfo.committedState != newDisplayInfo.committedState) { + mask |= LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED; + } return mask; } /** @@ -1360,6 +1368,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { return "disconnected"; case LOGICAL_DISPLAY_EVENT_STATE_CHANGED: return "state_changed"; + case LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED: + return "committed_state_changed"; case LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED: return "refresh_rate_changed"; case LOGICAL_DISPLAY_EVENT_BASIC_CHANGED: diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index 4779b690adfb..e7939bb50ece 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -371,7 +371,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mCallback = callback; mProjection = projection; mMediaProjectionCallback = mediaProjectionCallback; - mDisplayState = Display.STATE_ON; + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + // The display's power state depends on the power state of the state of its + // display / power group, which we don't know here. Initializing to UNKNOWN allows + // the first call to requestDisplayStateLocked() to set the correct state. + // This also triggers VirtualDisplay.Callback to tell the owner the initial state. + mDisplayState = Display.STATE_UNKNOWN; + } else { + mDisplayState = Display.STATE_ON; + } mPendingChanges |= PENDING_SURFACE_CHANGE; mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror(); mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled(); @@ -564,14 +572,23 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mInfo.yDpi = mDensityDpi; mInfo.presentationDeadlineNanos = 1000000000L / (int) getRefreshRate(); // 1 frame mInfo.flags = 0; - if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { - mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE - | DisplayDeviceInfo.FLAG_NEVER_BLANK; - } - if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { - mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK; + if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()) { + if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE; + } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + } } else { - mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + if ((mFlags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE + | DisplayDeviceInfo.FLAG_NEVER_BLANK; + } + if ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) { + mInfo.flags &= ~DisplayDeviceInfo.FLAG_NEVER_BLANK; + } else { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + } } if ((mFlags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) { mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP; diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index bc5d90599b41..e4b595ab7c55 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -280,6 +280,11 @@ public class DisplayManagerFlags { Flags::refreshRateEventForForegroundApps ); + private final FlagState mCommittedStateSeparateEvent = new FlagState( + Flags.FLAG_COMMITTED_STATE_SEPARATE_EVENT, + Flags::committedStateSeparateEvent + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -603,6 +608,14 @@ public class DisplayManagerFlags { } /** + * @return {@code true} if the flag for having a separate event for display's committed state + * is enabled + */ + public boolean isCommittedStateSeparateEventEnabled() { + return mCommittedStateSeparateEvent.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ @@ -659,6 +672,7 @@ public class DisplayManagerFlags { pw.println(" " + mBaseDensityForExternalDisplays); pw.println(" " + mFramerateOverrideTriggersRrCallbacks); pw.println(" " + mRefreshRateEventForForegroundApps); + pw.println(" " + mCommittedStateSeparateEvent); } private static class FlagState { diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 8211febade60..acdc0e0cf891 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -456,9 +456,8 @@ flag { flag { name: "enable_display_content_mode_management" namespace: "lse_desktop_experience" - description: "Enable switching the content mode of connected displays between mirroring and extened. Also change the default content mode to extended mode." + description: "Enable switching the content mode of connected displays between mirroring and extended. Also change the default content mode to extended mode." bug: "378385869" - is_fixed_read_only: true } flag { @@ -509,3 +508,14 @@ flag { bug: "293651324" is_fixed_read_only: false } + +flag { + name: "committed_state_separate_event" + namespace: "display_manager" + description: "Move Display committed state into a separate event" + bug: "342192387" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 2af74f620c95..7e8bb28b6a37 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -569,8 +569,7 @@ public final class DreamManagerService extends SystemService { } private void requestDreamInternal() { - if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront() - && !isDozingInternal()) { + if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) { return; } diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java index 94842041af82..ab86433ca50d 100644 --- a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java +++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java @@ -58,9 +58,9 @@ public interface AudioDeviceVolumeManagerWrapper { void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment); + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener); /** * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeAdjustOnlyBehavior( @@ -69,7 +69,7 @@ public interface AudioDeviceVolumeManagerWrapper { void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment); + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener); } diff --git a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java index ff99ace38ef0..10cbb00d2398 100644 --- a/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java +++ b/services/core/java/com/android/server/hdmi/DefaultAudioDeviceVolumeManagerWrapper.java @@ -61,21 +61,21 @@ public class DefaultAudioDeviceVolumeManagerWrapper public void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { - mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor, - vclistener, handlesVolumeAdjustment); + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) { + mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, + handlesVolumeAdjustment, executor, vclistener); } @Override public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener) { mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeAdjustOnlyBehavior(device, volume, - executor, vclistener, handlesVolumeAdjustment); + handlesVolumeAdjustment, executor, vclistener); } } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 3d6d34bf9911..3cb21c3e2697 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -1404,6 +1404,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { if (connected) { if (mArcEstablished) { enableAudioReturnChannel(true); + } else { + HdmiLogger.debug("Restart ARC again"); + onNewAvrAdded(getAvrDeviceInfo()); } } else { enableAudioReturnChannel(false); diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 89f0d0edbf2b..6d973ac8d1b5 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -4798,15 +4798,15 @@ public class HdmiControlService extends SystemService { Slog.d(TAG, "Enabling absolute volume behavior"); for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) { getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior( - device, volumeInfo, mServiceThreadExecutor, - mAbsoluteVolumeChangedListener, true); + device, volumeInfo, true, mServiceThreadExecutor, + mAbsoluteVolumeChangedListener); } } else if (tv() != null) { Slog.d(TAG, "Enabling adjust-only absolute volume behavior"); for (AudioDeviceAttributes device : getAvbCapableAudioOutputDevices()) { getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeAdjustOnlyBehavior( - device, volumeInfo, mServiceThreadExecutor, - mAbsoluteVolumeChangedListener, true); + device, volumeInfo, true, mServiceThreadExecutor, + mAbsoluteVolumeChangedListener); } } diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 87f693cc7291..1ace41cba364 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -344,4 +344,42 @@ public abstract class InputManagerInternal { */ public abstract void applyBackupPayload(Map<Integer, byte[]> payload, int userId) throws XmlPullParserException, IOException; + + /** + * An interface for filtering pointer motion event before cursor position is determined. + * <p> + * Different from {@code android.view.InputFilter}, this filter can filter motion events at + * an early stage of the input pipeline, but only called for pointer's relative motion events. + * Unless the user really needs to filter events before the cursor position in the display is + * determined, use {@code android.view.InputFilter} instead. + */ + public interface AccessibilityPointerMotionFilter { + /** + * Called everytime pointer's relative motion event happens. + * The returned dx and dy will be used to move the cursor in the display. + * <p> + * This call happens on the input hot path and it is extremely performance sensitive. It + * also must not call back into native code. + * + * @param dx delta x of the event in pixels. + * @param dy delta y of the event in pixels. + * @param currentX the cursor x coordinate on the screen before the motion event. + * @param currentY the cursor y coordinate on the screen before the motion event. + * @param displayId the display ID of the current cursor. + * @return an array of length 2, delta x and delta y after filtering the motion. The delta + * values are in pixels and must be between 0 and original delta. + */ + @NonNull + float[] filterPointerMotionEvent(float dx, float dy, float currentX, float currentY, + int displayId); + } + + /** + * Registers an {@code AccessibilityCursorFilter}. + * + * @param filter The filter to register. If a filter is already registered, the old filter is + * unregistered. {@code null} unregisters the filter that is already registered. + */ + public abstract void registerAccessibilityPointerMotionFilter( + @Nullable AccessibilityPointerMotionFilter filter); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 8624f4230e9c..0e37238bcb84 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -25,8 +25,8 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static com.android.hardware.input.Flags.enableCustomizableInputGestures; -import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.keyEventActivityDetection; +import static com.android.hardware.input.Flags.touchpadVisualizer; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER; @@ -193,15 +193,11 @@ public class InputManagerService extends IInputManager.Stub private static final int MSG_SYSTEM_READY = 5; private static final int DEFAULT_VIBRATION_MAGNITUDE = 192; - private static final AdditionalDisplayInputProperties - DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties(); private final NativeInputManagerService mNative; private final Context mContext; private final InputManagerHandler mHandler; - @UserIdInt - private int mCurrentUserId = UserHandle.USER_SYSTEM; private DisplayManagerInternal mDisplayManagerInternal; private WindowManagerInternal mWindowManagerInternal; @@ -289,7 +285,7 @@ public class InputManagerService extends IInputManager.Stub final Object mKeyEventActivityLock = new Object(); @GuardedBy("mKeyEventActivityLock") - private List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify = + private final List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify = new ArrayList<>(); // Rate limit for key event activity detection. Prevent the listener from being notified @@ -460,6 +456,14 @@ public class InputManagerService extends IInputManager.Stub private boolean mShowKeyPresses = false; private boolean mShowRotaryInput = false; + /** + * A lock for the accessibility pointer motion filter. Don't call native methods while holding + * this lock. + */ + private final Object mAccessibilityPointerMotionFilterLock = new Object(); + private InputManagerInternal.AccessibilityPointerMotionFilter + mAccessibilityPointerMotionFilter = null; + /** Point of injection for test dependencies. */ @VisibleForTesting static class Injector { @@ -2593,6 +2597,23 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") + final float[] filterPointerMotion(float dx, float dy, float currentX, float currentY, + int displayId) { + // This call happens on the input hot path and it is extremely performance sensitive. + // This must not call back into native code. This is called while the + // PointerChoreographer's lock is held. + synchronized (mAccessibilityPointerMotionFilterLock) { + if (mAccessibilityPointerMotionFilter == null) { + throw new IllegalStateException( + "filterCursor is invoked but no callback is registered."); + } + return mAccessibilityPointerMotionFilter.filterPointerMotionEvent(dx, dy, currentX, + currentY, displayId); + } + } + + // Native callback. + @SuppressWarnings("unused") @VisibleForTesting public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { notifyKeyActivityListeners(event); @@ -3215,7 +3236,6 @@ public class InputManagerService extends IInputManager.Stub } private void handleCurrentUserChanged(@UserIdInt int userId) { - mCurrentUserId = userId; mKeyGestureController.setCurrentUserId(userId); } @@ -3828,6 +3848,12 @@ public class InputManagerService extends IInputManager.Stub payload.get(BACKUP_CATEGORY_INPUT_GESTURES), userId); } } + + @Override + public void registerAccessibilityPointerMotionFilter( + AccessibilityPointerMotionFilter filter) { + InputManagerService.this.registerAccessibilityPointerMotionFilter(filter); + } } @Override @@ -4014,6 +4040,26 @@ public class InputManagerService extends IInputManager.Stub mPointerIconCache.setAccessibilityScaleFactor(displayId, scaleFactor); } + void registerAccessibilityPointerMotionFilter( + InputManagerInternal.AccessibilityPointerMotionFilter filter) { + // `#filterPointerMotion` expects that when it's called, `mAccessibilityPointerMotionFilter` + // is not null. + // Also, to avoid potential lock contention, we shouldn't call native method while holding + // the lock here. Native code calls `#filterPointerMotion` while PointerChoreographer's + // lock is held. + // Thus, we must set filter before we enable the filter in native, and reset the filter + // after we disable the filter. + // This also ensures the previously installed filter isn't called after the filter is + // updated. + mNative.setAccessibilityPointerMotionFilterEnabled(false); + synchronized (mAccessibilityPointerMotionFilterLock) { + mAccessibilityPointerMotionFilter = filter; + } + if (filter != null) { + mNative.setAccessibilityPointerMotionFilterEnabled(true); + } + } + interface KeyboardBacklightControllerInterface { default void incrementKeyboardBacklight(int deviceId) {} default void decrementKeyboardBacklight(int deviceId) {} diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index f34338a397db..32409d39db3b 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -315,6 +315,16 @@ interface NativeInputManagerService { */ boolean setKernelWakeEnabled(int deviceId, boolean enabled); + /** + * Set whether the accessibility pointer motion filter is enabled. + * <p> + * Once enabled, {@link InputManagerService#filterPointerMotion} is called for evety motion + * event from pointer devices. + * + * @param enabled {@code true} if the filter is enabled, {@code false} otherwise. + */ + void setAccessibilityPointerMotionFilterEnabled(boolean enabled); + /** The native implementation of InputManagerService methods. */ class NativeImpl implements NativeInputManagerService { /** Pointer to native input manager service object, used by native code. */ @@ -628,5 +638,8 @@ interface NativeInputManagerService { @Override public native boolean setKernelWakeEnabled(int deviceId, boolean enabled); + + @Override + public native void setAccessibilityPointerMotionFilterEnabled(boolean enabled); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 484b47022f04..508bc2f811e0 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -365,7 +365,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return mCurrentImeUserId; } - /** + /** * Figures out the target IME user ID associated with the given {@code displayId}. * * @param displayId the display ID to be queried about @@ -649,12 +649,25 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. visibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard( accessibilitySoftKeyboardSetting); if (visibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) { - hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, - 0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, userId); + if (Flags.refactorInsetsController()) { + final var statsToken = createStatsTokenForFocusedClient(false /* show */, + SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, userId); + setImeVisibilityOnFocusedWindowClient(false, userData, statsToken); + } else { + hideCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, + 0 /* flags */, SoftInputShowHideReason.HIDE_SETTINGS_ON_CHANGE, + userId); + } } else if (isShowRequestedForCurrentWindow(userId)) { - showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, - InputMethodManager.SHOW_IMPLICIT, - SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId); + if (Flags.refactorInsetsController()) { + final var statsToken = createStatsTokenForFocusedClient(true /* show */, + SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId); + setImeVisibilityOnFocusedWindowClient(true, userData, statsToken); + } else { + showCurrentInputLocked(userData.mImeBindingState.mFocusedWindow, + InputMethodManager.SHOW_IMPLICIT, + SoftInputShowHideReason.SHOW_SETTINGS_ON_CHANGE, userId); + } } break; } @@ -1319,8 +1332,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = bindingController.getSelectedMethodId(); final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (selectedMethodId != null && settings.getMethodMap().get(selectedMethodId) != null - && !settings.getMethodMap().get(selectedMethodId).isSystem()) { + final InputMethodInfo selectedImi = settings.getMethodMap().get(selectedMethodId); + if (selectedImi != null && !selectedImi.isSystem()) { return; } final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java index 42303e042561..b735b2447486 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java @@ -141,8 +141,7 @@ final class MediaRoute2ProviderWatcher { isSelfScanOnlyProvider |= MediaRoute2ProviderService.CATEGORY_SELF_SCAN_ONLY.equals(category); supportsSystemMediaRouting |= - MediaRoute2ProviderService.SERVICE_INTERFACE_SYSTEM_MEDIA.equals( - category); + MediaRoute2ProviderService.CATEGORY_SYSTEM_MEDIA.equals(category); } } int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 6c0d8ad7264d..debac9436bb3 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -1635,11 +1635,11 @@ class MediaRouter2ServiceImpl { manager)); } - List<MediaRoute2Info> routes = - userRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters.values().stream() - .toList(); userRecord.mHandler.sendMessage( - obtainMessage(ManagerRecord::notifyRoutesUpdated, managerRecord, routes)); + obtainMessage( + UserHandler::dispatchRoutesToManagerOnHandler, + userRecord.mHandler, + managerRecord)); } @GuardedBy("mLock") @@ -2119,6 +2119,9 @@ class MediaRouter2ServiceImpl { mHasBluetoothRoutingPermission.set(checkCallerHasBluetoothPermissions(mPid, mUid)); boolean newSystemRoutingPermissionValue = hasSystemRoutingPermission(); if (oldSystemRoutingPermissionValue != newSystemRoutingPermissionValue) { + // TODO: b/379788233 - Ensure access to fields like + // mLastNotifiedRoutesToPrivilegedRouters happens on the right thread. We might need + // to run this on the handler. Map<String, MediaRoute2Info> routesToReport = newSystemRoutingPermissionValue ? mUserRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters @@ -2543,6 +2546,8 @@ class MediaRouter2ServiceImpl { * both system route providers and user route providers. * * <p>See {@link #getRouterRecords(boolean hasModifyAudioRoutingPermission)}. + * + * <p>Must be accessed on this handler's thread. */ private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToPrivilegedRouters = new ArrayMap<>(); @@ -2558,6 +2563,8 @@ class MediaRouter2ServiceImpl { * (e.g. volume changes) to non-privileged routers. * * <p>See {@link SystemMediaRoute2Provider#mDefaultRoute}. + * + * <p>Must be accessed on this handler's thread. */ private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToNonPrivilegedRouters = new ArrayMap<>(); @@ -2800,7 +2807,7 @@ class MediaRouter2ServiceImpl { removedRoutes)); } - dispatchUpdates( + dispatchUpdatesOnHandler( hasAddedOrModifiedRoutes, hasRemovedRoutes, provider.mIsSystemRouteProvider, @@ -2822,6 +2829,13 @@ class MediaRouter2ServiceImpl { source, providerId, routesString); } + /** Notifies the given manager of the current routes. */ + public void dispatchRoutesToManagerOnHandler(ManagerRecord managerRecord) { + List<MediaRoute2Info> routes = + mLastNotifiedRoutesToPrivilegedRouters.values().stream().toList(); + managerRecord.notifyRoutesUpdated(routes); + } + /** * Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters} * and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link @@ -2834,7 +2848,7 @@ class MediaRouter2ServiceImpl { * @param isSystemProvider whether the latest update was caused by a system provider. * @param defaultRoute the current default route in {@link #mSystemProvider}. */ - private void dispatchUpdates( + private void dispatchUpdatesOnHandler( boolean hasAddedOrModifiedRoutes, boolean hasRemovedRoutes, boolean isSystemProvider, diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 0b8b115e65d0..91a2843ccaf7 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -120,6 +120,8 @@ public class MediaQualityService extends SystemService { private final Object mPictureProfileLock = new Object(); // A global lock for sound profile objects. private final Object mSoundProfileLock = new Object(); + // A global lock for user state objects. + private final Object mUserStateLock = new Object(); // A global lock for ambient backlight objects. private final Object mAmbientBacklightLock = new Object(); @@ -127,17 +129,17 @@ public class MediaQualityService extends SystemService { super(context); mContext = context; mHalAmbientBacklightCallback = new HalAmbientBacklightCallback(); - mPictureProfileAdjListener = new PictureProfileAdjustmentListenerImpl(mContext); - mSoundProfileAdjListener = new SoundProfileAdjustmentListenerImpl(mContext); mPackageManager = mContext.getPackageManager(); mPictureProfileTempIdMap = new BiMap<>(); mSoundProfileTempIdMap = new BiMap<>(); mMediaQualityDbHelper = new MediaQualityDbHelper(mContext); - mMqDatabaseUtils = new MqDatabaseUtils(mContext); mMediaQualityDbHelper.setWriteAheadLoggingEnabled(true); mMediaQualityDbHelper.setIdleConnectionTimeout(30); - mHalNotifier = new HalNotifier(); mMqManagerNotifier = new MqManagerNotifier(); + mMqDatabaseUtils = new MqDatabaseUtils(); + mHalNotifier = new HalNotifier(); + mPictureProfileAdjListener = new PictureProfileAdjustmentListenerImpl(); + mSoundProfileAdjListener = new SoundProfileAdjustmentListenerImpl(); // The package info in the context isn't initialized in the way it is for normal apps, // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we @@ -166,20 +168,21 @@ public class MediaQualityService extends SystemService { if (mMediaQuality != null) { try { mMediaQuality.setAmbientBacklightCallback(mHalAmbientBacklightCallback); + + mPpChangedListener = mMediaQuality.getPictureProfileListener(); + mSpChangedListener = mMediaQuality.getSoundProfileListener(); + mMediaQuality.setPictureProfileAdjustmentListener(mPictureProfileAdjListener); mMediaQuality.setSoundProfileAdjustmentListener(mSoundProfileAdjListener); + } catch (RemoteException e) { Slog.e(TAG, "Failed to set ambient backlight detector callback", e); } } - mPpChangedListener = IPictureProfileChangedListener.Stub.asInterface(binder); - mSpChangedListener = ISoundProfileChangedListener.Stub.asInterface(binder); - publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService()); } - // TODO: Add additional APIs. b/373951081 private final class BinderService extends IMediaQualityManager.Stub { @GuardedBy("mPictureProfileLock") @@ -225,7 +228,6 @@ public class MediaQualityService extends SystemService { PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } - synchronized (mPictureProfileLock) { ContentValues values = MediaQualityUtils.getContentValues(dbId, pp.getProfileType(), @@ -233,7 +235,6 @@ public class MediaQualityService extends SystemService { pp.getPackageName(), pp.getInputId(), pp.getParameters()); - updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, pp.getParameters()); } } @@ -269,12 +270,13 @@ public class MediaQualityService extends SystemService { mMqManagerNotifier.notifyOnPictureProfileError(id, PictureProfile.ERROR_INVALID_ARGUMENT, Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnPictureProfileRemoved( + mPictureProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mPictureProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); } - mMqManagerNotifier.notifyOnPictureProfileRemoved( - mPictureProfileTempIdMap.getValue(dbId), toDelete, - Binder.getCallingUid(), Binder.getCallingPid()); - mPictureProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnPictureProfileChange(dbId, null); } } } @@ -520,12 +522,13 @@ public class MediaQualityService extends SystemService { mMqManagerNotifier.notifyOnSoundProfileError(id, SoundProfile.ERROR_INVALID_ARGUMENT, Binder.getCallingUid(), Binder.getCallingPid()); + } else { + mMqManagerNotifier.notifyOnSoundProfileRemoved( + mSoundProfileTempIdMap.getValue(dbId), toDelete, + Binder.getCallingUid(), Binder.getCallingPid()); + mSoundProfileTempIdMap.remove(dbId); + mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); } - mMqManagerNotifier.notifyOnSoundProfileRemoved( - mSoundProfileTempIdMap.getValue(dbId), toDelete, - Binder.getCallingUid(), Binder.getCallingPid()); - mSoundProfileTempIdMap.remove(dbId); - mHalNotifier.notifyHalOnSoundProfileChange(dbId, null); } } } @@ -684,24 +687,22 @@ public class MediaQualityService extends SystemService { mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED; } - //TODO: need lock here? @Override public void registerPictureProfileCallback(final IPictureProfileCallback callback) { int callingPid = Binder.getCallingPid(); int callingUid = Binder.getCallingUid(); - UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid()); + UserState userState = getOrCreateUserState(Binder.getCallingUid()); userState.mPictureProfileCallbackPidUidMap.put(callback, Pair.create(callingPid, callingUid)); } - //TODO: need lock here? @Override public void registerSoundProfileCallback(final ISoundProfileCallback callback) { int callingPid = Binder.getCallingPid(); int callingUid = Binder.getCallingUid(); - UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid()); + UserState userState = getOrCreateUserState(Binder.getCallingUid()); userState.mSoundProfileCallbackPidUidMap.put(callback, Pair.create(callingPid, callingUid)); } @@ -792,7 +793,6 @@ public class MediaQualityService extends SystemService { } } - //TODO: do I need a lock here? @Override public List<ParameterCapability> getParameterCapabilities( List<String> names, UserHandle user) { @@ -809,14 +809,20 @@ public class MediaQualityService extends SystemService { private List<ParameterCapability> getListParameterCapability(ParamCapability[] caps) { List<ParameterCapability> pcList = new ArrayList<>(); - for (ParamCapability pcHal : caps) { - String name = MediaQualityUtils.getParameterName(pcHal.name); - boolean isSupported = pcHal.isSupported; - int type = pcHal.defaultValue == null ? 0 : pcHal.defaultValue.getTag() + 1; - Bundle bundle = MediaQualityUtils.convertToCaps(type, pcHal.range); - pcList.add(new ParameterCapability(name, isSupported, type, bundle)); + if (caps != null) { + for (ParamCapability pcHal : caps) { + if (pcHal != null) { + String name = MediaQualityUtils.getParameterName(pcHal.name); + boolean isSupported = pcHal.isSupported; + int type = pcHal.defaultValue == null ? 0 : pcHal.defaultValue.getTag() + 1; + Bundle bundle = MediaQualityUtils.convertToCaps(type, pcHal.range); + + pcList.add(new ParameterCapability(name, isSupported, type, bundle)); + } + } } + return pcList; } @@ -1055,7 +1061,7 @@ public class MediaQualityService extends SystemService { synchronized (mPictureProfileLock) { for (int i = 0; i < mUserStates.size(); i++) { int userId = mUserStates.keyAt(i); - UserState userState = getOrCreateUserStateLocked(userId); + UserState userState = getOrCreateUserState(userId); userState.mPictureProfileCallbackPidUidMap.remove(callback); } } @@ -1069,7 +1075,7 @@ public class MediaQualityService extends SystemService { synchronized (mSoundProfileLock) { for (int i = 0; i < mUserStates.size(); i++) { int userId = mUserStates.keyAt(i); - UserState userState = getOrCreateUserStateLocked(userId); + UserState userState = getOrCreateUserState(userId); userState.mSoundProfileCallbackPidUidMap.remove(callback); } } @@ -1095,25 +1101,27 @@ public class MediaQualityService extends SystemService { } } - //TODO: used by both picture and sound. can i add both locks? - private UserState getOrCreateUserStateLocked(int userId) { - UserState userState = getUserStateLocked(userId); + @GuardedBy("mUserStateLock") + private UserState getOrCreateUserState(int userId) { + UserState userState = getUserState(userId); if (userState == null) { userState = new UserState(mContext, userId); - mUserStates.put(userId, userState); + synchronized (mUserStateLock) { + mUserStates.put(userId, userState); + } } return userState; } - //TODO: used by both picture and sound. can i add both locks? - private UserState getUserStateLocked(int userId) { - return mUserStates.get(userId); + @GuardedBy("mUserStateLock") + private UserState getUserState(int userId) { + synchronized (mUserStateLock) { + return mUserStates.get(userId); + } } private final class MqDatabaseUtils { - MediaQualityDbHelper mMediaQualityDbHelper; - private PictureProfile getPictureProfile(Long dbId) { String selection = BaseParameters.PARAMETER_ID + " = ?"; String[] selectionArguments = {Long.toString(dbId)}; @@ -1205,8 +1213,7 @@ public class MediaQualityService extends SystemService { /*groupBy=*/ null, /*having=*/ null, /*orderBy=*/ null); } - private MqDatabaseUtils(Context context) { - mMediaQualityDbHelper = new MediaQualityDbHelper(context); + private MqDatabaseUtils() { } } @@ -1264,7 +1271,7 @@ public class MediaQualityService extends SystemService { private void notifyPictureProfileHelper(int mode, String profileId, PictureProfile profile, Integer errorCode, List<ParameterCapability> paramCaps, int uid, int pid) { - UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM); int n = userState.mPictureProfileCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { @@ -1349,7 +1356,7 @@ public class MediaQualityService extends SystemService { private void notifySoundProfileHelper(int mode, String profileId, SoundProfile profile, Integer errorCode, List<ParameterCapability> paramCaps, int uid, int pid) { - UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM); int n = userState.mSoundProfileCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { @@ -1404,11 +1411,13 @@ public class MediaQualityService extends SystemService { private void notifyHalOnPictureProfileChange(Long dbId, PersistableBundle params) { // TODO: only notify HAL when the profile is active / being used - try { - mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId, - params)); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to notify HAL on picture profile change.", e); + if (mPpChangedListener != null) { + try { + mPpChangedListener.onPictureProfileChanged(convertToHalPictureProfile(dbId, + params)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify HAL on picture profile change.", e); + } } } @@ -1429,10 +1438,13 @@ public class MediaQualityService extends SystemService { private void notifyHalOnSoundProfileChange(Long dbId, PersistableBundle params) { // TODO: only notify HAL when the profile is active / being used - try { - mSpChangedListener.onSoundProfileChanged(convertToHalSoundProfile(dbId, params)); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to notify HAL on sound profile change.", e); + if (mSpChangedListener != null) { + try { + mSpChangedListener + .onSoundProfileChanged(convertToHalSoundProfile(dbId, params)); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify HAL on sound profile change.", e); + } } } @@ -1488,9 +1500,6 @@ public class MediaQualityService extends SystemService { private final class PictureProfileAdjustmentListenerImpl extends IPictureProfileAdjustmentListener.Stub { - MqDatabaseUtils mMqDatabaseUtils; - MqManagerNotifier mMqManagerNotifier; - HalNotifier mHalNotifier; @Override public void onPictureProfileAdjusted( @@ -1542,18 +1551,13 @@ public class MediaQualityService extends SystemService { return null; } - private PictureProfileAdjustmentListenerImpl(Context context) { - mMqDatabaseUtils = new MqDatabaseUtils(context); - mMqManagerNotifier = new MqManagerNotifier(); - mHalNotifier = new HalNotifier(); + private PictureProfileAdjustmentListenerImpl() { + } } private final class SoundProfileAdjustmentListenerImpl extends ISoundProfileAdjustmentListener.Stub { - MqDatabaseUtils mMqDatabaseUtils; - MqManagerNotifier mMqManagerNotifier; - HalNotifier mHalNotifier; @Override public void onSoundProfileAdjusted( @@ -1598,10 +1602,8 @@ public class MediaQualityService extends SystemService { return null; } - private SoundProfileAdjustmentListenerImpl(Context context) { - mMqDatabaseUtils = new MqDatabaseUtils(context); - mMqManagerNotifier = new MqManagerNotifier(); - mHalNotifier = new HalNotifier(); + private SoundProfileAdjustmentListenerImpl() { + } } diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index b0ef80793cd7..62e26e189a35 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -25,6 +25,8 @@ import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; +import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER; +import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser; import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled; import android.annotation.FlaggedApi; @@ -75,7 +77,9 @@ import com.android.internal.util.XmlUtils; import com.android.internal.util.function.TriPredicate; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.LocalServices; import com.android.server.notification.NotificationManagerService.DumpFilter; +import com.android.server.pm.UserManagerInternal; import com.android.server.utils.TimingsTraceAndSlog; import org.xmlpull.v1.XmlPullParser; @@ -134,6 +138,7 @@ abstract public class ManagedServices { private final UserProfiles mUserProfiles; protected final IPackageManager mPm; protected final UserManager mUm; + protected final UserManagerInternal mUmInternal; private final Config mConfig; private final Handler mHandler = new Handler(Looper.getMainLooper()); @@ -157,12 +162,17 @@ abstract public class ManagedServices { protected final ArraySet<String> mDefaultPackages = new ArraySet<>(); // lists the component names of all enabled (and therefore potentially connected) - // app services for current profiles. + // app services for each user. This is intended to support a concurrent multi-user environment. + // key value is the resolved userId. @GuardedBy("mMutex") - private final ArraySet<ComponentName> mEnabledServicesForCurrentProfiles = new ArraySet<>(); - // Just the packages from mEnabledServicesForCurrentProfiles + private final SparseArray<ArraySet<ComponentName>> mEnabledServicesByUser = + new SparseArray<>(); + // Just the packages from mEnabledServicesByUser + // This is intended to support a concurrent multi-user environment. + // key value is the resolved userId. @GuardedBy("mMutex") - private final ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>(); + private final SparseArray<ArraySet<String>> mEnabledServicesPackageNamesByUser = + new SparseArray<>(); // Per user id, list of enabled packages that have nevertheless asked not to be run @GuardedBy("mSnoozing") private final SparseSetArray<ComponentName> mSnoozing = new SparseSetArray<>(); @@ -195,6 +205,10 @@ abstract public class ManagedServices { mConfig = getConfig(); mApprovalLevel = APPROVAL_BY_COMPONENT; mUm = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mUmInternal = LocalServices.getService(UserManagerInternal.class); + // Initialize for the current user. + mEnabledServicesByUser.put(UserHandle.USER_CURRENT, new ArraySet<>()); + mEnabledServicesPackageNamesByUser.put(UserHandle.USER_CURRENT, new ArraySet<>()); } abstract protected Config getConfig(); @@ -383,11 +397,30 @@ abstract public class ManagedServices { } synchronized (mMutex) { - pw.println(" All " + getCaption() + "s (" + mEnabledServicesForCurrentProfiles.size() - + ") enabled for current profiles:"); - for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) { - if (filter != null && !filter.matches(cmpt)) continue; - pw.println(" " + cmpt); + if (managedServicesConcurrentMultiuser()) { + for (int i = 0; i < mEnabledServicesByUser.size(); i++) { + final int userId = mEnabledServicesByUser.keyAt(i); + final ArraySet<ComponentName> componentNames = + mEnabledServicesByUser.get(userId); + String userString = userId == UserHandle.USER_CURRENT + ? "current profiles" : "user " + Integer.toString(userId); + pw.println(" All " + getCaption() + "s (" + componentNames.size() + + ") enabled for " + userString + ":"); + for (ComponentName cmpt : componentNames) { + if (filter != null && !filter.matches(cmpt)) continue; + pw.println(" " + cmpt); + } + } + } else { + final ArraySet<ComponentName> enabledServicesForCurrentProfiles = + mEnabledServicesByUser.get(UserHandle.USER_CURRENT); + pw.println(" All " + getCaption() + "s (" + + enabledServicesForCurrentProfiles.size() + + ") enabled for current profiles:"); + for (ComponentName cmpt : enabledServicesForCurrentProfiles) { + if (filter != null && !filter.matches(cmpt)) continue; + pw.println(" " + cmpt); + } } pw.println(" Live " + getCaption() + "s (" + mServices.size() + "):"); @@ -442,11 +475,24 @@ abstract public class ManagedServices { } } - synchronized (mMutex) { - for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) { - if (filter != null && !filter.matches(cmpt)) continue; - cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED); + if (managedServicesConcurrentMultiuser()) { + for (int i = 0; i < mEnabledServicesByUser.size(); i++) { + final int userId = mEnabledServicesByUser.keyAt(i); + final ArraySet<ComponentName> componentNames = + mEnabledServicesByUser.get(userId); + for (ComponentName cmpt : componentNames) { + if (filter != null && !filter.matches(cmpt)) continue; + cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED); + } + } + } else { + final ArraySet<ComponentName> enabledServicesForCurrentProfiles = + mEnabledServicesByUser.get(UserHandle.USER_CURRENT); + for (ComponentName cmpt : enabledServicesForCurrentProfiles) { + if (filter != null && !filter.matches(cmpt)) continue; + cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED); + } } for (ManagedServiceInfo info : mServices) { if (filter != null && !filter.matches(info.component)) continue; @@ -841,9 +887,31 @@ abstract public class ManagedServices { } } + /** convenience method for looking in mEnabledServicesPackageNamesByUser + * for UserHandle.USER_CURRENT. + * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes + * trunk stable, this API should be deprecated. Additionally, when this method + * is deprecated, the unit tests written using this method should also be revised. + * + * @param pkg target package name + * @return boolean value that indicates whether it is enabled for the current profiles + */ protected boolean isComponentEnabledForPackage(String pkg) { + return isComponentEnabledForPackage(pkg, UserHandle.USER_CURRENT); + } + + /** convenience method for looking in mEnabledServicesPackageNamesByUser + * + * @param pkg target package name + * @param userId the id of the target user + * @return boolean value that indicates whether it is enabled for the target user + */ + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + protected boolean isComponentEnabledForPackage(String pkg, int userId) { synchronized (mMutex) { - return mEnabledServicesPackageNames.contains(pkg); + ArraySet<String> enabledServicesPackageNames = + mEnabledServicesPackageNamesByUser.get(resolveUserId(userId)); + return enabledServicesPackageNames != null && enabledServicesPackageNames.contains(pkg); } } @@ -1016,9 +1084,14 @@ abstract public class ManagedServices { public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uidList) { if (DEBUG) { synchronized (mMutex) { + int resolvedUserId = (managedServicesConcurrentMultiuser() + && (uidList != null && uidList.length > 0)) + ? resolveUserId(UserHandle.getUserId(uidList[0])) + : UserHandle.USER_CURRENT; Slog.d(TAG, "onPackagesChanged removingPackage=" + removingPackage + " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList)) - + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames); + + " mEnabledServicesPackageNames=" + + mEnabledServicesPackageNamesByUser.get(resolvedUserId)); } } @@ -1034,11 +1107,18 @@ abstract public class ManagedServices { } } for (String pkgName : pkgList) { - if (isComponentEnabledForPackage(pkgName)) { - anyServicesInvolved = true; + if (!managedServicesConcurrentMultiuser()) { + if (isComponentEnabledForPackage(pkgName)) { + anyServicesInvolved = true; + } } if (uidList != null && uidList.length > 0) { for (int uid : uidList) { + if (managedServicesConcurrentMultiuser()) { + if (isComponentEnabledForPackage(pkgName, UserHandle.getUserId(uid))) { + anyServicesInvolved = true; + } + } if (isPackageAllowed(pkgName, UserHandle.getUserId(uid))) { anyServicesInvolved = true; trimApprovedListsForInvalidServices(pkgName, UserHandle.getUserId(uid)); @@ -1065,6 +1145,36 @@ abstract public class ManagedServices { unbindUserServices(user); } + /** + * Call this method when a user is stopped + * + * @param user the id of the stopped user + */ + public void onUserStopped(int user) { + if (!managedServicesConcurrentMultiuser()) { + return; + } + boolean hasAny = false; + synchronized (mMutex) { + if (mEnabledServicesByUser.contains(user) + && mEnabledServicesPackageNamesByUser.contains(user)) { + // Through the ManagedServices.resolveUserId, + // we resolve UserHandle.USER_CURRENT as the key for users + // other than the visible background user. + // Therefore, the user IDs that exist as keys for each member variable + // correspond to the visible background user. + // We need to unbind services of the stopped visible background user. + mEnabledServicesByUser.remove(user); + mEnabledServicesPackageNamesByUser.remove(user); + hasAny = true; + } + } + if (hasAny) { + Slog.i(TAG, "Removing approved services for stopped user " + user); + unbindUserServices(user); + } + } + public void onUserSwitched(int user) { if (DEBUG) Slog.d(TAG, "onUserSwitched u=" + user); unbindOtherUserServices(user); @@ -1386,19 +1496,42 @@ abstract public class ManagedServices { protected void populateComponentsToBind(SparseArray<Set<ComponentName>> componentsToBind, final IntArray activeUsers, SparseArray<ArraySet<ComponentName>> approvedComponentsByUser) { - mEnabledServicesForCurrentProfiles.clear(); - mEnabledServicesPackageNames.clear(); final int nUserIds = activeUsers.size(); - + if (managedServicesConcurrentMultiuser()) { + for (int i = 0; i < nUserIds; ++i) { + final int resolvedUserId = resolveUserId(activeUsers.get(i)); + if (mEnabledServicesByUser.get(resolvedUserId) != null) { + mEnabledServicesByUser.get(resolvedUserId).clear(); + } + if (mEnabledServicesPackageNamesByUser.get(resolvedUserId) != null) { + mEnabledServicesPackageNamesByUser.get(resolvedUserId).clear(); + } + } + } else { + mEnabledServicesByUser.get(UserHandle.USER_CURRENT).clear(); + mEnabledServicesPackageNamesByUser.get(UserHandle.USER_CURRENT).clear(); + } for (int i = 0; i < nUserIds; ++i) { - // decode the list of components final int userId = activeUsers.get(i); + // decode the list of components final ArraySet<ComponentName> userComponents = approvedComponentsByUser.get(userId); if (null == userComponents) { componentsToBind.put(userId, new ArraySet<>()); continue; } + final int resolvedUserId = managedServicesConcurrentMultiuser() + ? resolveUserId(userId) + : UserHandle.USER_CURRENT; + ArraySet<ComponentName> enabledServices = + mEnabledServicesByUser.contains(resolvedUserId) + ? mEnabledServicesByUser.get(resolvedUserId) + : new ArraySet<>(); + ArraySet<String> enabledServicesPackageName = + mEnabledServicesPackageNamesByUser.contains(resolvedUserId) + ? mEnabledServicesPackageNamesByUser.get(resolvedUserId) + : new ArraySet<>(); + final Set<ComponentName> add = new HashSet<>(userComponents); synchronized (mSnoozing) { ArraySet<ComponentName> snoozed = mSnoozing.get(userId); @@ -1409,12 +1542,12 @@ abstract public class ManagedServices { componentsToBind.put(userId, add); - mEnabledServicesForCurrentProfiles.addAll(userComponents); - + enabledServices.addAll(userComponents); for (int j = 0; j < userComponents.size(); j++) { - final ComponentName component = userComponents.valueAt(j); - mEnabledServicesPackageNames.add(component.getPackageName()); + enabledServicesPackageName.add(userComponents.valueAt(j).getPackageName()); } + mEnabledServicesByUser.put(resolvedUserId, enabledServices); + mEnabledServicesPackageNamesByUser.put(resolvedUserId, enabledServicesPackageName); } } @@ -1453,13 +1586,9 @@ abstract public class ManagedServices { */ protected void rebindServices(boolean forceRebind, int userToRebind) { if (DEBUG) Slog.d(TAG, "rebindServices " + forceRebind + " " + userToRebind); - IntArray userIds = mUserProfiles.getCurrentProfileIds(); boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext) && allowRebindForParentUser(); - if (userToRebind != USER_ALL && !rebindAllCurrentUsers) { - userIds = new IntArray(1); - userIds.add(userToRebind); - } + IntArray userIds = getUserIdsForRebindServices(userToRebind, rebindAllCurrentUsers); final SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>(); @@ -1483,6 +1612,23 @@ abstract public class ManagedServices { bindToServices(componentsToBind); } + private IntArray getUserIdsForRebindServices(int userToRebind, boolean rebindAllCurrentUsers) { + IntArray userIds = mUserProfiles.getCurrentProfileIds(); + if (userToRebind != USER_ALL && !rebindAllCurrentUsers) { + userIds = new IntArray(1); + userIds.add(userToRebind); + } else if (managedServicesConcurrentMultiuser() + && userToRebind == USER_ALL) { + for (UserInfo user : mUm.getUsers()) { + if (mUmInternal.isVisibleBackgroundFullUser(user.id) + && !userIds.contains(user.id)) { + userIds.add(user.id); + } + } + } + return userIds; + } + /** * Called when user switched to unbind all services from other users. */ @@ -1506,7 +1652,11 @@ abstract public class ManagedServices { synchronized (mMutex) { final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices(); for (ManagedServiceInfo info : removableBoundServices) { - if ((allExceptUser && (info.userid != user)) + // User switching is the event for the forground user. + // It should not affect the service of the visible background user. + if ((allExceptUser && (info.userid != user) + && !(managedServicesConcurrentMultiuser() + && info.isVisibleBackgroundUserService)) || (!allExceptUser && (info.userid == user))) { Set<ComponentName> toUnbind = componentsToUnbind.get(info.userid, new ArraySet<>()); @@ -1861,6 +2011,29 @@ abstract public class ManagedServices { } /** + * This method returns the mapped id for the incoming user id + * If the incoming id was not the id of the visible background user, it returns USER_CURRENT. + * In the other cases, it returns the same value as the input. + * + * @param userId the id of the user + * @return the user id if it is a visible background user, otherwise + * {@link UserHandle#USER_CURRENT} + */ + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + @VisibleForTesting + public int resolveUserId(int userId) { + if (managedServicesConcurrentMultiuser()) { + if (mUmInternal.isVisibleBackgroundFullUser(userId)) { + // The dataset of the visible background user should be managed independently. + return userId; + } + } + // The data of current user and its profile users need to be managed + // in a dataset as before. + return UserHandle.USER_CURRENT; + } + + /** * Returns true if services in the parent user should be rebound * when rebindServices is called with a profile userId. * Must be false for NotificationAssistants. @@ -1878,6 +2051,8 @@ abstract public class ManagedServices { public int targetSdkVersion; public Pair<ComponentName, Integer> mKey; public int uid; + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public boolean isVisibleBackgroundUserService; public ManagedServiceInfo(IInterface service, ComponentName component, int userid, boolean isSystem, ServiceConnection connection, int targetSdkVersion, @@ -1889,6 +2064,10 @@ abstract public class ManagedServices { this.connection = connection; this.targetSdkVersion = targetSdkVersion; this.uid = uid; + if (managedServicesConcurrentMultiuser()) { + this.isVisibleBackgroundUserService = LocalServices + .getService(UserManagerInternal.class).isVisibleBackgroundFullUser(userid); + } mKey = Pair.create(component, userid); } @@ -1937,19 +2116,28 @@ abstract public class ManagedServices { } public boolean isSameUser(int userId) { - if (!isEnabledForCurrentProfiles()) { + if (!isEnabledForUser()) { return false; } return userId == USER_ALL || userId == this.userid; } public boolean enabledAndUserMatches(int nid) { - if (!isEnabledForCurrentProfiles()) { + if (!isEnabledForUser()) { return false; } if (this.userid == USER_ALL) return true; if (this.isSystem) return true; if (nid == USER_ALL || nid == this.userid) return true; + if (managedServicesConcurrentMultiuser() + && mUmInternal.getProfileParentId(nid) + != mUmInternal.getProfileParentId(this.userid)) { + // If the profile parent IDs do not match each other, + // it is determined that the users do not match. + // This situation may occur when comparing the current user's ID + // with the visible background user's ID. + return false; + } return supportsProfiles() && mUserProfiles.isCurrentProfile(nid) && isPermittedForProfile(nid); @@ -1969,12 +2157,21 @@ abstract public class ManagedServices { removeServiceImpl(this.service, this.userid); } - /** convenience method for looking in mEnabledServicesForCurrentProfiles */ - public boolean isEnabledForCurrentProfiles() { + /** + * convenience method for looking in mEnabledServicesByUser. + * If FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER is disabled, this manages the data using + * only UserHandle.USER_CURRENT as the key, in order to behave the same as the legacy logic. + */ + public boolean isEnabledForUser() { if (this.isSystem) return true; if (this.connection == null) return false; synchronized (mMutex) { - return mEnabledServicesForCurrentProfiles.contains(this.component); + int resolvedUserId = managedServicesConcurrentMultiuser() + ? resolveUserId(this.userid) + : UserHandle.USER_CURRENT; + ArraySet<ComponentName> enabledServices = + mEnabledServicesByUser.get(resolvedUserId); + return enabledServices != null && enabledServices.contains(this.component); } } @@ -2017,10 +2214,30 @@ abstract public class ManagedServices { } } - /** convenience method for looking in mEnabledServicesForCurrentProfiles */ + /** convenience method for looking in mEnabledServicesByUser for UserHandle.USER_CURRENT. + * This is a legacy API. When FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER becomes + * trunk stable, this API should be deprecated. Additionally, when this method + * is deprecated, the unit tests written using this method should also be revised. + * + * @param component target component name + * @return boolean value that indicates whether it is enabled for the current profiles + */ public boolean isComponentEnabledForCurrentProfiles(ComponentName component) { + return isComponentEnabledForUser(component, UserHandle.USER_CURRENT); + } + + /** convenience method for looking in mEnabledServicesForUser + * + * @param component target component name + * @param userId the id of the target user + * @return boolean value that indicates whether it is enabled for the target user + */ + @FlaggedApi(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public boolean isComponentEnabledForUser(ComponentName component, int userId) { synchronized (mMutex) { - return mEnabledServicesForCurrentProfiles.contains(component); + ArraySet<ComponentName> enabledServicesForUser = + mEnabledServicesByUser.get(resolveUserId(userId)); + return enabledServicesForUser != null && enabledServicesForUser.contains(component); } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 340afb776405..6fddfb5f90a7 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -173,6 +173,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; import static com.android.server.notification.Flags.expireBitmaps; +import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT; import static com.android.server.utils.PriorityDump.PRIORITY_ARG; @@ -1207,7 +1208,7 @@ public class NotificationManagerService extends SystemService { } mAssistants.resetDefaultAssistantsIfNecessary(); - mPreferencesHelper.syncChannelsBypassingDnd(); + mPreferencesHelper.syncHasPriorityChannels(); } @VisibleForTesting @@ -2323,6 +2324,9 @@ public class NotificationManagerService extends SystemService { if (userHandle >= 0) { cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle, REASON_USER_STOPPED); + mConditionProviders.onUserStopped(userHandle); + mListeners.onUserStopped(userHandle); + mAssistants.onUserStopped(userHandle); } } else if ( isProfileUnavailable(action)) { @@ -2343,7 +2347,7 @@ public class NotificationManagerService extends SystemService { mConditionProviders.onUserSwitched(userId); mListeners.onUserSwitched(userId); mZenModeHelper.onUserSwitched(userId); - mPreferencesHelper.syncChannelsBypassingDnd(); + mPreferencesHelper.syncHasPriorityChannels(); } // assistant is the only thing that cares about managed profiles specifically mAssistants.onUserSwitched(userId); @@ -2367,7 +2371,7 @@ public class NotificationManagerService extends SystemService { mConditionProviders.onUserRemoved(userId); mAssistants.onUserRemoved(userId); mHistoryManager.onUserRemoved(userId); - mPreferencesHelper.syncChannelsBypassingDnd(); + mPreferencesHelper.syncHasPriorityChannels(); handleSavePolicyFile(); } else if (action.equals(Intent.ACTION_USER_UNLOCKED)) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); @@ -2376,9 +2380,6 @@ public class NotificationManagerService extends SystemService { if (!mUserProfiles.isProfileUser(userId, context)) { mConditionProviders.onUserUnlocked(userId); mListeners.onUserUnlocked(userId); - if (!android.app.Flags.modesApi()) { - mZenModeHelper.onUserUnlocked(userId); - } } } } @@ -2767,9 +2768,7 @@ public class NotificationManagerService extends SystemService { void onPolicyChanged(Policy newPolicy) { Binder.withCleanCallingIdentity(() -> { Intent intent = new Intent(ACTION_NOTIFICATION_POLICY_CHANGED); - if (android.app.Flags.modesApi()) { - intent.putExtra(EXTRA_NOTIFICATION_POLICY, newPolicy); - } + intent.putExtra(EXTRA_NOTIFICATION_POLICY, newPolicy); sendRegisteredOnlyBroadcast(intent); mRankingHandler.requestSort(); }); @@ -2778,11 +2777,10 @@ public class NotificationManagerService extends SystemService { @Override void onConsolidatedPolicyChanged(Policy newConsolidatedPolicy) { Binder.withCleanCallingIdentity(() -> { - if (android.app.Flags.modesApi()) { - Intent intent = new Intent(ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED); - intent.putExtra(EXTRA_NOTIFICATION_POLICY, newConsolidatedPolicy); - sendRegisteredOnlyBroadcast(intent); - } + Intent intent = new Intent(ACTION_CONSOLIDATED_NOTIFICATION_POLICY_CHANGED); + intent.putExtra(EXTRA_NOTIFICATION_POLICY, newConsolidatedPolicy); + sendRegisteredOnlyBroadcast(intent); + mRankingHandler.requestSort(); }); } @@ -3368,7 +3366,7 @@ public class NotificationManagerService extends SystemService { migrateDefaultNAS(); maybeShowInitialReviewPermissionsNotification(); - if (android.app.Flags.modesApi() && !mZenModeHelper.hasDeviceEffectsApplier()) { + if (!mZenModeHelper.hasDeviceEffectsApplier()) { // Cannot be done earlier, as some services aren't ready until this point. mZenModeHelper.setDeviceEffectsApplier( new DefaultDeviceEffectsApplier(getContext())); @@ -3446,7 +3444,7 @@ public class NotificationManagerService extends SystemService { mConditionProviders.onUserSwitched(userId); mListeners.onUserSwitched(userId); mZenModeHelper.onUserSwitched(userId); - mPreferencesHelper.syncChannelsBypassingDnd(); + mPreferencesHelper.syncHasPriorityChannels(); } // assistant is the only thing that cares about managed profiles specifically mAssistants.onUserSwitched(userId); @@ -5236,11 +5234,8 @@ public class NotificationManagerService extends SystemService { @Override public boolean areChannelsBypassingDnd() { - if (android.app.Flags.modesApi()) { - return mZenModeHelper.getConsolidatedNotificationPolicy().allowPriorityChannels() - && mPreferencesHelper.areChannelsBypassingDnd(); - } - return mPreferencesHelper.areChannelsBypassingDnd(); + return mZenModeHelper.getConsolidatedNotificationPolicy().allowPriorityChannels() + && mPreferencesHelper.hasPriorityChannels(); } @Override @@ -5730,12 +5725,13 @@ public class NotificationManagerService extends SystemService { public void requestBindListener(ComponentName component) { checkCallerIsSystemOrSameApp(component.getPackageName()); int uid = Binder.getCallingUid(); + int userId = UserHandle.getUserId(uid); final long identity = Binder.clearCallingIdentity(); try { - ManagedServices manager = - mAssistants.isComponentEnabledForCurrentProfiles(component) - ? mAssistants - : mListeners; + boolean isAssistantEnabled = managedServicesConcurrentMultiuser() + ? mAssistants.isComponentEnabledForUser(component, userId) + : mAssistants.isComponentEnabledForCurrentProfiles(component); + ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners; manager.setComponentState(component, UserHandle.getUserId(uid), true); } finally { Binder.restoreCallingIdentity(identity); @@ -5762,16 +5758,16 @@ public class NotificationManagerService extends SystemService { public void requestUnbindListenerComponent(ComponentName component) { checkCallerIsSameApp(component.getPackageName()); int uid = Binder.getCallingUid(); + int userId = UserHandle.getUserId(uid); final long identity = Binder.clearCallingIdentity(); try { synchronized (mNotificationLock) { - ManagedServices manager = - mAssistants.isComponentEnabledForCurrentProfiles(component) - ? mAssistants - : mListeners; - if (manager.isPackageOrComponentAllowed(component.flattenToString(), - UserHandle.getUserId(uid))) { - manager.setComponentState(component, UserHandle.getUserId(uid), false); + boolean isAssistantEnabled = managedServicesConcurrentMultiuser() + ? mAssistants.isComponentEnabledForUser(component, userId) + : mAssistants.isComponentEnabledForCurrentProfiles(component); + ManagedServices manager = isAssistantEnabled ? mAssistants : mListeners; + if (manager.isPackageOrComponentAllowed(component.flattenToString(), userId)) { + manager.setComponentState(component, userId, false); } } } finally { @@ -6092,43 +6088,27 @@ public class NotificationManagerService extends SystemService { @Override public void requestInterruptionFilterFromListener(INotificationListener token, int interruptionFilter) throws RemoteException { - if (android.app.Flags.modesApi()) { - final int callingUid = Binder.getCallingUid(); - ManagedServiceInfo info; - synchronized (mNotificationLock) { - info = mListeners.checkServiceTokenLocked(token); - } + final int callingUid = Binder.getCallingUid(); + ManagedServiceInfo info; + synchronized (mNotificationLock) { + info = mListeners.checkServiceTokenLocked(token); + } - final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1); - if (zenMode == -1) return; + final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1); + if (zenMode == -1) return; - UserHandle zenUser = getCallingZenUser(); - if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) { - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule( - zenUser, info.component.getPackageName(), callingUid, zenMode); - } else { - int origin = computeZenOrigin(/* fromUser= */ false); - Binder.withCleanCallingIdentity(() -> { - mZenModeHelper.setManualZenMode(zenUser, zenMode, /* conditionId= */ null, - origin, "listener:" + info.component.flattenToShortString(), - /* caller= */ info.component.getPackageName(), - callingUid); - }); - } + UserHandle zenUser = getCallingZenUser(); + if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) { + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule( + zenUser, info.component.getPackageName(), callingUid, zenMode); } else { - final int callingUid = Binder.getCallingUid(); - final boolean isSystemOrSystemUi = isCallerSystemOrSystemUi(); - final long identity = Binder.clearCallingIdentity(); - try { - synchronized (mNotificationLock) { - final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token); - mZenModeHelper.requestFromListener(info.component, interruptionFilter, - callingUid, isSystemOrSystemUi); - updateInterruptionFilterLocked(); - } - } finally { - Binder.restoreCallingIdentity(identity); - } + int origin = computeZenOrigin(/* fromUser= */ false); + Binder.withCleanCallingIdentity(() -> { + mZenModeHelper.setManualZenMode(zenUser, zenMode, /* conditionId= */ null, + origin, "listener:" + info.component.flattenToShortString(), + /* caller= */ info.component.getPackageName(), + callingUid); + }); } } @@ -6177,19 +6157,8 @@ public class NotificationManagerService extends SystemService { } } - // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined. - @Override - public List<ZenModeConfig.ZenRule> getZenRules() throws RemoteException { - int callingUid = Binder.getCallingUid(); - enforcePolicyAccess(callingUid, "getZenRules"); - return mZenModeHelper.getZenRules(getCallingZenUser(), callingUid); - } - @Override public Map<String, AutomaticZenRule> getAutomaticZenRules() { - if (!android.app.Flags.modesApi()) { - throw new IllegalStateException("getAutomaticZenRules called with flag off!"); - } int callingUid = Binder.getCallingUid(); enforcePolicyAccess(callingUid, "getAutomaticZenRules"); return mZenModeHelper.getAutomaticZenRules(getCallingZenUser(), callingUid); @@ -6260,50 +6229,40 @@ public class NotificationManagerService extends SystemService { // Implicit rules have no ConditionProvider or Activity. We allow the user to customize // them (via Settings), but not the owner app. Should the app want to start using it as // a "normal" rule, it must provide a CP/ConfigActivity too. - if (android.app.Flags.modesApi()) { - boolean isImplicitRuleUpdateFromSystem = updateId != null - && ZenModeConfig.isImplicitRuleId(updateId) - && isCallerSystemOrSystemUi(); - if (!isImplicitRuleUpdateFromSystem - && rule.getOwner() == null - && rule.getConfigurationActivity() == null) { - throw new NullPointerException( - "Rule must have a ConditionProviderService and/or configuration " - + "activity"); - } - } else { - if (rule.getOwner() == null && rule.getConfigurationActivity() == null) { - throw new NullPointerException( - "Rule must have a ConditionProviderService and/or configuration " - + "activity"); - } + boolean isImplicitRuleUpdateFromSystem = updateId != null + && ZenModeConfig.isImplicitRuleId(updateId) + && isCallerSystemOrSystemUi(); + if (!isImplicitRuleUpdateFromSystem + && rule.getOwner() == null + && rule.getConfigurationActivity() == null) { + throw new NullPointerException( + "Rule must have a ConditionProviderService and/or configuration " + + "activity"); } Objects.requireNonNull(rule.getConditionId(), "ConditionId is null"); - if (android.app.Flags.modesApi()) { - if (isCallerSystemOrSystemUi()) { - return; // System callers can use any type. - } - int uid = Binder.getCallingUid(); - int userId = UserHandle.getUserId(uid); + if (isCallerSystemOrSystemUi()) { + return; // System callers can use any type. + } + int uid = Binder.getCallingUid(); + int userId = UserHandle.getUserId(uid); - if (rule.getType() == AutomaticZenRule.TYPE_MANAGED) { - boolean isDeviceOwner = Binder.withCleanCallingIdentity( - () -> mDpm.isActiveDeviceOwner(uid)); - if (!isDeviceOwner) { - throw new IllegalArgumentException( - "Only Device Owners can use AutomaticZenRules with TYPE_MANAGED"); - } - } else if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME) { - String wellbeingPackage = getContext().getResources().getString( - com.android.internal.R.string.config_systemWellbeing); - boolean isCallerWellbeing = !TextUtils.isEmpty(wellbeingPackage) - && mPackageManagerInternal.isSameApp(wellbeingPackage, uid, userId); - if (!isCallerWellbeing) { - throw new IllegalArgumentException( - "Only the 'Wellbeing' package can use AutomaticZenRules with " - + "TYPE_BEDTIME"); - } + if (rule.getType() == AutomaticZenRule.TYPE_MANAGED) { + boolean isDeviceOwner = Binder.withCleanCallingIdentity( + () -> mDpm.isActiveDeviceOwner(uid)); + if (!isDeviceOwner) { + throw new IllegalArgumentException( + "Only Device Owners can use AutomaticZenRules with TYPE_MANAGED"); + } + } else if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME) { + String wellbeingPackage = getContext().getResources().getString( + com.android.internal.R.string.config_systemWellbeing); + boolean isCallerWellbeing = !TextUtils.isEmpty(wellbeingPackage) + && mPackageManagerInternal.isSameApp(wellbeingPackage, uid, userId); + if (!isCallerWellbeing) { + throw new IllegalArgumentException( + "Only the 'Wellbeing' package can use AutomaticZenRules with " + + "TYPE_BEDTIME"); } } } @@ -6386,9 +6345,7 @@ public class NotificationManagerService extends SystemService { @ZenModeConfig.ConfigOrigin private int computeZenOrigin(boolean fromUser) { - // "fromUser" is introduced with MODES_API, so only consider it in that case. - // (Non-MODES_API behavior should also not depend at all on ORIGIN_USER_IN_X). - if (android.app.Flags.modesApi() && fromUser) { + if (fromUser) { if (isCallerSystemOrSystemUi()) { return ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; } else { @@ -6402,9 +6359,7 @@ public class NotificationManagerService extends SystemService { } private void enforceUserOriginOnlyFromSystem(boolean fromUser, String method) { - if (android.app.Flags.modesApi() - && fromUser - && !isCallerSystemOrSystemUiOrShell()) { + if (fromUser && !isCallerSystemOrSystemUiOrShell()) { throw new SecurityException(TextUtils.formatSimple( "Calling %s with fromUser == true is only allowed for system", method)); } @@ -6419,7 +6374,7 @@ public class NotificationManagerService extends SystemService { enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter"); UserHandle zenUser = getCallingZenUser(); - if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) { + if (!canManageGlobalZenPolicy(pkg, callingUid)) { mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(zenUser, pkg, callingUid, zen); return; } @@ -6549,6 +6504,13 @@ public class NotificationManagerService extends SystemService { } catch (NameNotFoundException e) { return false; } + if (managedServicesConcurrentMultiuser()) { + return checkPackagePolicyAccess(pkg) + || mListeners.isComponentEnabledForPackage(pkg, + UserHandle.getCallingUserId()) + || (mDpm != null + && (mDpm.isActiveProfileOwner(uid) || mDpm.isActiveDeviceOwner(uid))); + } //TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode. return checkPackagePolicyAccess(pkg) || mListeners.isComponentEnabledForPackage(pkg) @@ -6723,7 +6685,7 @@ public class NotificationManagerService extends SystemService { public Policy getNotificationPolicy(String pkg) { final int callingUid = Binder.getCallingUid(); UserHandle zenUser = getCallingZenUser(); - if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) { + if (!canManageGlobalZenPolicy(pkg, callingUid)) { return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(zenUser, pkg); } final long identity = Binder.clearCallingIdentity(); @@ -6760,8 +6722,7 @@ public class NotificationManagerService extends SystemService { UserHandle zenUser = getCallingZenUser(); boolean isSystemCaller = isCallerSystemOrSystemUiOrShell(); - boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi() - && !canManageGlobalZenPolicy(pkg, callingUid); + boolean shouldApplyAsImplicitRule = !canManageGlobalZenPolicy(pkg, callingUid); final long identity = Binder.clearCallingIdentity(); try { @@ -6953,7 +6914,8 @@ public class NotificationManagerService extends SystemService { android.Manifest.permission.INTERACT_ACROSS_USERS, "setNotificationListenerAccessGrantedForUser for user " + userId); } - if (mUmInternal.isVisibleBackgroundFullUser(userId)) { + if (!managedServicesConcurrentMultiuser() + && mUmInternal.isVisibleBackgroundFullUser(userId)) { // The main use case for visible background users is the Automotive multi-display // configuration where a passenger can use a secondary display while the driver is // using the main display. NotificationListeners is designed only for the current @@ -8219,9 +8181,6 @@ public class NotificationManagerService extends SystemService { @Override public void setDeviceEffectsApplier(DeviceEffectsApplier applier) { - if (!android.app.Flags.modesApi()) { - return; - } if (mZenModeHelper == null) { throw new IllegalStateException("ZenModeHelper is not yet ready!"); } @@ -13165,7 +13124,8 @@ public class NotificationManagerService extends SystemService { @Override public void onUserUnlocked(int user) { - if (mUmInternal.isVisibleBackgroundFullUser(user)) { + if (!managedServicesConcurrentMultiuser() + && mUmInternal.isVisibleBackgroundFullUser(user)) { // The main use case for visible background users is the Automotive // multi-display configuration where a passenger can use a secondary // display while the driver is using the main display. @@ -13805,7 +13765,7 @@ public class NotificationManagerService extends SystemService { // TODO (b/73052211): if the ranking update changed the notification type, // cancel notifications for NLSes that can't see them anymore for (final ManagedServiceInfo serviceInfo : getServices()) { - if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener( + if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener( serviceInfo, ActivityManager.getCurrentUser())) { continue; } @@ -13833,7 +13793,7 @@ public class NotificationManagerService extends SystemService { @GuardedBy("mNotificationLock") public void notifyListenerHintsChangedLocked(final int hints) { for (final ManagedServiceInfo serviceInfo : getServices()) { - if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener( + if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener( serviceInfo, ActivityManager.getCurrentUser())) { continue; } @@ -13889,7 +13849,7 @@ public class NotificationManagerService extends SystemService { public void notifyInterruptionFilterChanged(final int interruptionFilter) { for (final ManagedServiceInfo serviceInfo : getServices()) { - if (!serviceInfo.isEnabledForCurrentProfiles() || !isInteractionVisibleToListener( + if (!serviceInfo.isEnabledForUser() || !isInteractionVisibleToListener( serviceInfo, ActivityManager.getCurrentUser())) { continue; } diff --git a/services/core/java/com/android/server/notification/NotificationShellCmd.java b/services/core/java/com/android/server/notification/NotificationShellCmd.java index c305d66c24c1..bc987ed21251 100644 --- a/services/core/java/com/android/server/notification/NotificationShellCmd.java +++ b/services/core/java/com/android/server/notification/NotificationShellCmd.java @@ -183,13 +183,8 @@ public class NotificationShellCmd extends ShellCommand { interruptionFilter = INTERRUPTION_FILTER_ALL; } final int filter = interruptionFilter; - if (android.app.Flags.modesApi()) { - mBinderService.setInterruptionFilter(callingPackage, filter, - /* fromUser= */ true); - } else { - mBinderService.setInterruptionFilter(callingPackage, filter, - /* fromUser= */ false); - } + mBinderService.setInterruptionFilter(callingPackage, filter, + /* fromUser= */ true); } break; case "allow_dnd": { diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 3974c839fd38..0fc182f3f1bb 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -233,11 +233,9 @@ public class PreferencesHelper implements RankingConfig { private SparseBooleanArray mLockScreenShowNotifications; private SparseBooleanArray mLockScreenPrivateNotifications; private boolean mIsMediaNotificationFilteringEnabled; - // When modes_api flag is enabled, this value only tracks whether the current user has any - // channels marked as "priority channels", but not necessarily whether they are permitted - // to bypass DND by current zen policy. - // TODO: b/310620812 - Rename to be more accurate when modes_api flag is inlined. - private boolean mCurrentUserHasChannelsBypassingDnd; + // Whether the current user has any channels marked as "priority channels" -- but not + // necessarily whether they are permitted to bypass DND by current zen policy. + private boolean mCurrentUserHasPriorityChannels; private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS; private final boolean mShowReviewPermissionsNotification; @@ -1063,7 +1061,7 @@ public class PreferencesHelper implements RankingConfig { r.groups.put(group.getId(), group); } if (needsDndChange) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi); } if (android.app.Flags.nmBinderPerfCacheChannels() && changed) { invalidateNotificationChannelGroupCache(); @@ -1150,7 +1148,7 @@ public class PreferencesHelper implements RankingConfig { existing.setBypassDnd(bypassDnd); needsPolicyFileChange = true; - if (bypassDnd != mCurrentUserHasChannelsBypassingDnd + if (bypassDnd != mCurrentUserHasPriorityChannels || previousExistingImportance != existing.getImportance()) { needsDndChange = true; } @@ -1214,7 +1212,7 @@ public class PreferencesHelper implements RankingConfig { } r.channels.put(channel.getId(), channel); - if (channel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd) { + if (channel.canBypassDnd() != mCurrentUserHasPriorityChannels) { needsDndChange = true; } MetricsLogger.action(getChannelLog(channel, pkg).setType( @@ -1224,7 +1222,7 @@ public class PreferencesHelper implements RankingConfig { } if (needsDndChange) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi); } if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) { @@ -1317,14 +1315,14 @@ public class PreferencesHelper implements RankingConfig { // relevantly affected without the parent channel already having been. } - if (updatedChannel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd + if (updatedChannel.canBypassDnd() != mCurrentUserHasPriorityChannels || channel.getImportance() != updatedChannel.getImportance()) { needsDndChange = true; changed = true; } } if (needsDndChange) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi); } if (changed) { if (android.app.Flags.nmBinderPerfCacheChannels()) { @@ -1550,7 +1548,7 @@ public class PreferencesHelper implements RankingConfig { } } if (channelBypassedDnd) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi); } if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) { @@ -1745,7 +1743,7 @@ public class PreferencesHelper implements RankingConfig { } } if (groupBypassedDnd) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi); } if (android.app.Flags.nmBinderPerfCacheChannels()) { if (deletedChannels.size() > 0) { @@ -1906,8 +1904,8 @@ public class PreferencesHelper implements RankingConfig { } } if (!deletedChannelIds.isEmpty()) { - if (mCurrentUserHasChannelsBypassingDnd) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + if (mCurrentUserHasPriorityChannels) { + updateCurrentUserHasPriorityChannels(callingUid, fromSystemOrSystemUi); } if (android.app.Flags.nmBinderPerfCacheChannels()) { invalidateNotificationChannelCache(); @@ -2098,7 +2096,7 @@ public class PreferencesHelper implements RankingConfig { } /** - * Syncs {@link #mCurrentUserHasChannelsBypassingDnd} with the current user's notification + * Syncs {@link #mCurrentUserHasPriorityChannels} with the current user's notification * policy before updating. Must be called: * <ul> * <li>On system init, after channels and DND configurations are loaded. @@ -2106,22 +2104,23 @@ public class PreferencesHelper implements RankingConfig { * <li>If users are removed (the removed user could've been a profile of the current one). * </ul> */ - void syncChannelsBypassingDnd() { - mCurrentUserHasChannelsBypassingDnd = + void syncHasPriorityChannels() { + mCurrentUserHasPriorityChannels = (mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).state - & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0; + & NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS) != 0; - updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID, + updateCurrentUserHasPriorityChannels(/* callingUid= */ Process.SYSTEM_UID, /* fromSystemOrSystemUi= */ true); } /** * Updates the user's NotificationPolicy based on whether the current userId has channels - * bypassing DND. It should be called whenever a channel is created, updated, or deleted, or - * when the current user (or its profiles) change. + * marked as "priority" (which might bypass DND, depending on the zen rule details). It should + * be called whenever a channel is created, updated, or deleted, or when the current user (or + * its profiles) change. */ // TODO: b/368247671 - remove fromSystemOrSystemUi argument when modes_ui is inlined. - private void updateCurrentUserHasChannelsBypassingDnd(int callingUid, + private void updateCurrentUserHasPriorityChannels(int callingUid, boolean fromSystemOrSystemUi) { ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>(); @@ -2149,13 +2148,13 @@ public class PreferencesHelper implements RankingConfig { } } boolean haveBypassingApps = candidatePkgs.size() > 0; - if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) { - mCurrentUserHasChannelsBypassingDnd = haveBypassingApps; + if (mCurrentUserHasPriorityChannels != haveBypassingApps) { + mCurrentUserHasPriorityChannels = haveBypassingApps; if (android.app.Flags.modesUi()) { mZenModeHelper.updateHasPriorityChannels(UserHandle.CURRENT, - mCurrentUserHasChannelsBypassingDnd); + mCurrentUserHasPriorityChannels); } else { - updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid, + updateZenPolicy(mCurrentUserHasPriorityChannels, callingUid, fromSystemOrSystemUi); } } @@ -2188,16 +2187,20 @@ public class PreferencesHelper implements RankingConfig { policy.priorityCategories, policy.priorityCallSenders, policy.priorityMessageSenders, policy.suppressedVisualEffects, (areChannelsBypassingDnd - ? NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND : 0), + ? NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS : 0), policy.priorityConversationSenders), fromSystemOrSystemUi ? ZenModeConfig.ORIGIN_SYSTEM : ZenModeConfig.ORIGIN_APP, callingUid); } - // TODO: b/310620812 - rename to hasPriorityChannels() when modes_api is inlined. - public boolean areChannelsBypassingDnd() { - return mCurrentUserHasChannelsBypassingDnd; + /** + * Whether the current user has any channels marked as "priority channels" + * ({@link NotificationChannel#canBypassDnd}), but not necessarily whether they are permitted + * to bypass the filters set by the current zen policy. + */ + public boolean hasPriorityChannels() { + return mCurrentUserHasPriorityChannels; } /** diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java index fcc5e9771f94..ec9a2db52de9 100644 --- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -16,7 +16,7 @@ package com.android.server.notification; -import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND; +import static android.app.NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS; import static android.provider.Settings.Global.ZEN_MODE_OFF; import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_NONE; import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_PRIORITY; @@ -285,11 +285,10 @@ class ZenModeEventLogger { return true; } - if (Flags.modesApi() && hasActiveRuleCountDiff()) { - // Rules with INTERRUPTION_FILTER_ALL were always possible but before MODES_API - // they were completely useless; now they can apply effects, so we want to log - // when they become active/inactive, even though DND itself (as in "notification - // blocking") is off. + if (hasActiveRuleCountDiff()) { + // Rules with INTERRUPTION_FILTER_ALL can apply effects, so we want to log when they + // become active/inactive, even though DND itself (as in "notification blocking") + // is off. return true; } @@ -331,7 +330,7 @@ class ZenModeEventLogger { } } - if (Flags.modesApi() && mNewZenMode == ZEN_MODE_OFF) { + if (mNewZenMode == ZEN_MODE_OFF) { // If the mode is OFF -> OFF then there cannot be any *effective* change to policy. // (Note that, in theory, a policy diff is impossible since we don't merge the // policies of INTERRUPTION_FILTER_ALL rules; this is a "just in case" check). @@ -439,24 +438,14 @@ class ZenModeEventLogger { // Determine the number of (automatic & manual) rules active after the change takes place. int getNumRulesActive() { - if (!Flags.modesApi()) { - // If the zen mode has turned off, that means nothing can be active. - if (mNewZenMode == ZEN_MODE_OFF) { - return 0; - } - } return numActiveRulesInConfig(mNewConfig); } /** - * Return a list of the types of each of the active rules in the configuration. - * Only available when {@code MODES_API} is active; otherwise returns an empty list. + * Return a list of the types of each of the active rules in the configuration (sorted by + * the numerical value of the type, and including duplicates). */ int[] getActiveRuleTypes() { - if (!Flags.modesApi()) { - return new int[0]; - } - ArrayList<Integer> activeTypes = new ArrayList<>(); List<ZenRule> activeRules = activeRulesList(mNewConfig); if (activeRules.size() == 0) { @@ -476,77 +465,10 @@ class ZenModeEventLogger { return out; } - /** - * Return our best guess as to whether the changes observed are due to a user action. - * Note that this (before {@code MODES_API}) won't be 100% accurate as we can't necessarily - * distinguish between a system uid call indicating "user interacted with Settings" vs "a - * system app changed something automatically". - */ + /** Return whether the changes observed are due to a user action. */ boolean getIsUserAction() { - if (Flags.modesApi()) { - return mOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI - || mOrigin == ZenModeConfig.ORIGIN_USER_IN_APP; - } - - // Approach for pre-MODES_API: - // - if manual rule turned on or off, the calling UID is system, and the new manual - // rule does not have an enabler set, guess that this is likely to be a user action. - // This may represent a system app turning on DND automatically, but we guess "user" - // in this case. - // - note that this has a known failure mode of "manual rule turning off - // automatically after the default time runs out". We currently have no way - // of distinguishing this case from a user manually turning off the rule. - // - the reason for checking the enabler field is that a call may look like it's - // coming from a system UID, but if an enabler is set then the request came - // from an external source. "enabler" will be blank when manual rule is turned - // on from Quick Settings or Settings. - // - if an automatic rule's state changes in whether it is "enabled", then - // that is probably a user action. - // - if an automatic rule goes from "not snoozing" to "snoozing", that is probably - // a user action; that means that the user temporarily turned off DND associated - // with that rule. - // - if an automatic rule becomes active but does *not* change in its enabled state - // (covered by a previous case anyway), we guess that this is an automatic change. - // - if a rule is added or removed and the call comes from the system, we guess that - // this is a user action (as system rules can't be added or removed without a user - // action). - switch (getChangedRuleType()) { - case RULE_TYPE_MANUAL: - // TODO(b/278888961): Distinguish the automatically-turned-off state - return isFromSystemOrSystemUi() && (getNewManualRuleEnabler() == null); - case RULE_TYPE_AUTOMATIC: - for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) { - if (d.wasAdded() || d.wasRemoved()) { - // If the change comes from system, a rule being added/removed indicates - // a likely user action. From an app, it's harder to know for sure. - return isFromSystemOrSystemUi(); - } - ZenModeDiff.FieldDiff enabled = d.getDiffForField( - ZenModeDiff.RuleDiff.FIELD_ENABLED); - if (enabled != null && enabled.hasDiff()) { - return true; - } - ZenModeDiff.FieldDiff snoozing = d.getDiffForField( - ZenModeDiff.RuleDiff.FIELD_SNOOZING); - if (snoozing != null && snoozing.hasDiff() && (boolean) snoozing.to()) { - return true; - } - } - // If the change was in an automatic rule and none of the "probably triggered - // by a user" cases apply, then it's probably an automatic change. - return false; - case RULE_TYPE_UNKNOWN: - default: - } - - // If the change wasn't in a rule, but was in the zen policy: consider to be user action - // if the calling uid is system - if (hasPolicyDiff() || hasChannelsBypassingDiff()) { - return mCallingUid == Process.SYSTEM_UID; - } - - // don't know, or none of the other things triggered; assume not a user action - return false; + return mOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI + || mOrigin == ZenModeConfig.ORIGIN_USER_IN_APP; } boolean isFromSystemOrSystemUi() { @@ -587,7 +509,7 @@ class ZenModeEventLogger { */ @Nullable byte[] getDNDPolicyProto() { - if (Flags.modesApi() && mNewZenMode == ZEN_MODE_OFF) { + if (mNewZenMode == ZEN_MODE_OFF) { return null; } @@ -628,13 +550,10 @@ class ZenModeEventLogger { mNewPolicy.allowMessagesFrom())); proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, mNewPolicy.allowConversationsFrom()); - - if (Flags.modesApi()) { - proto.write(DNDPolicyProto.ALLOW_CHANNELS, - mNewPolicy.allowPriorityChannels() - ? CHANNEL_POLICY_PRIORITY - : CHANNEL_POLICY_NONE); - } + proto.write(DNDPolicyProto.ALLOW_CHANNELS, + mNewPolicy.allowPriorityChannels() + ? CHANNEL_POLICY_PRIORITY + : CHANNEL_POLICY_NONE); } else { Log.wtf(TAG, "attempted to write zen mode log event with null policy"); } @@ -648,14 +567,14 @@ class ZenModeEventLogger { */ boolean getAreChannelsBypassing() { if (mNewPolicy != null) { - return (mNewPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0; + return (mNewPolicy.state & STATE_HAS_PRIORITY_CHANNELS) != 0; } return false; } private boolean hasChannelsBypassingDiff() { boolean prevChannelsBypassing = mPrevPolicy != null - ? (mPrevPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0 : false; + ? (mPrevPolicy.state & STATE_HAS_PRIORITY_CHANNELS) != 0 : false; return prevChannelsBypassing != getAreChannelsBypassing(); } diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java index bdca555707e3..87ae78195ff5 100644 --- a/services/core/java/com/android/server/notification/ZenModeFiltering.java +++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java @@ -19,7 +19,6 @@ package com.android.server.notification; import static android.provider.Settings.Global.ZEN_MODE_OFF; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; -import android.app.Flags; import android.app.Notification; import android.app.NotificationManager; import android.content.ComponentName; @@ -146,16 +145,12 @@ public class ZenModeFiltering { // Returns whether the record is permitted to bypass DND when the zen mode is // ZEN_MODE_IMPORTANT_INTERRUPTIONS. This depends on whether the record's package priority is - // marked as PRIORITY_MAX (an indication of it belonging to a priority channel), and, if - // the modes_api flag is on, whether the given policy permits priority channels to bypass. - // TODO: b/310620812 - simplify when modes_api is inlined. + // marked as PRIORITY_MAX (an indication of it belonging to a priority channel), and whether the + // given policy permits priority channels to bypass. private boolean canRecordBypassDnd(NotificationRecord record, NotificationManager.Policy policy) { boolean inPriorityChannel = record.getPackagePriority() == Notification.PRIORITY_MAX; - if (Flags.modesApi()) { - return inPriorityChannel && policy.allowPriorityChannels(); - } - return inPriorityChannel; + return inPriorityChannel && policy.allowPriorityChannels(); } /** diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index b39b6fde6258..889df512dd60 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -157,6 +157,12 @@ public class ZenModeHelper { static final int RULE_LIMIT_PER_PACKAGE = 100; private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30); + /** + * Amount of time since last activation after which implicit rules that have never been + * customized by the user are automatically cleaned up. + */ + private static final Duration IMPLICIT_RULE_KEPT_FOR = Duration.ofDays(30); + private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000; /** @@ -326,9 +332,6 @@ public class ZenModeHelper { * applied immediately. */ void setDeviceEffectsApplier(@NonNull DeviceEffectsApplier deviceEffectsApplier) { - if (!Flags.modesApi()) { - return; - } synchronized (mConfigLock) { if (mDeviceEffectsApplier != null) { throw new IllegalStateException("Already set up a DeviceEffectsApplier!"); @@ -350,11 +353,6 @@ public class ZenModeHelper { } } - // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers). - public void onUserUnlocked(int user) { - loadConfigForUser(user, "onUserUnlocked"); - } - void setPriorityOnlyDndExemptPackages(String[] packages) { mPriorityOnlyDndExemptPackages = packages; } @@ -385,21 +383,6 @@ public class ZenModeHelper { return NotificationManager.zenModeToInterruptionFilter(mZenMode); } - // TODO: b/310620812 - Remove when MODES_API is inlined (no more callers). - public void requestFromListener(ComponentName name, int filter, int callingUid, - boolean fromSystemOrSystemUi) { - final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); - if (newZen != -1) { - // This change is known to be for UserHandle.CURRENT because NLSes for - // background users are unbound. - setManualZenMode(UserHandle.CURRENT, newZen, null, - fromSystemOrSystemUi ? ORIGIN_SYSTEM : ORIGIN_APP, - /* reason= */ "listener:" + (name != null ? name.flattenToShortString() : null), - /* caller= */ name != null ? name.getPackageName() : null, - callingUid); - } - } - public void setSuppressedEffects(long suppressedEffects) { if (mSuppressedEffects == suppressedEffects) return; mSuppressedEffects = suppressedEffects; @@ -414,33 +397,24 @@ public class ZenModeHelper { return mZenMode; } - // TODO: b/310620812 - Make private (or inline) when MODES_API is inlined. - public List<ZenRule> getZenRules(UserHandle user, int callingUid) { - List<ZenRule> rules = new ArrayList<>(); + /** + * Get the list of {@link AutomaticZenRule} instances that the calling package can manage + * (which means the owned rules for a regular app, and every rule for system callers) together + * with their ids. + */ + Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user, int callingUid) { + HashMap<String, AutomaticZenRule> rules = new HashMap<>(); synchronized (mConfigLock) { ZenModeConfig config = getConfigLocked(user); if (config == null) return rules; + for (ZenRule rule : config.automaticRules.values()) { if (canManageAutomaticZenRule(rule, callingUid)) { - rules.add(rule); + rules.put(rule.id, zenRuleToAutomaticZenRule(rule)); } } + return rules; } - return rules; - } - - /** - * Get the list of {@link AutomaticZenRule} instances that the calling package can manage - * (which means the owned rules for a regular app, and every rule for system callers) together - * with their ids. - */ - Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user, int callingUid) { - List<ZenRule> ruleList = getZenRules(user, callingUid); - HashMap<String, AutomaticZenRule> rules = new HashMap<>(ruleList.size()); - for (ZenRule rule : ruleList) { - rules.put(rule.id, zenRuleToAutomaticZenRule(rule)); - } - return rules; } public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id, int callingUid) { @@ -511,9 +485,6 @@ public class ZenModeHelper { @GuardedBy("mConfigLock") private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, String pkg, ZenRule ruleToAdd, AutomaticZenRule azrToAdd, @ConfigOrigin int origin) { - if (!Flags.modesApi()) { - return ruleToAdd; - } String deletedKey = ZenModeConfig.deletedRuleKey(ruleToAdd); if (deletedKey == null) { // Couldn't calculate the deletedRuleKey (condition or pkg null?). This should @@ -561,9 +532,6 @@ public class ZenModeHelper { */ private static void maybeReplaceDefaultRule(ZenModeConfig config, @Nullable ZenRule oldRule, AutomaticZenRule rule) { - if (!Flags.modesApi()) { - return; - } if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME && (oldRule == null || oldRule.type != rule.getType())) { // Note: we must not verify canManageAutomaticZenRule here, since most likely they @@ -572,7 +540,7 @@ public class ZenModeHelper { ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); if (sleepingRule != null && !sleepingRule.enabled - && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) { + && !sleepingRule.isUserModified()) { config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); } } @@ -599,18 +567,10 @@ public class ZenModeHelper { } ZenModeConfig newConfig = config.copy(); ZenModeConfig.ZenRule newRule = requireNonNull(newConfig.automaticRules.get(ruleId)); - if (!Flags.modesApi()) { - if (newRule.enabled != automaticZenRule.isEnabled()) { - dispatchOnAutomaticRuleStatusChanged(config.user, newRule.getPkg(), ruleId, - automaticZenRule.isEnabled() - ? AUTOMATIC_RULE_STATUS_ENABLED - : AUTOMATIC_RULE_STATUS_DISABLED); - } - } boolean updated = populateZenRule(newRule.pkg, automaticZenRule, newConfig, newRule, origin, /* isNew= */ false); - if (Flags.modesApi() && !updated) { + if (!updated) { // Bail out so we don't have the side effects of updating a rule (i.e. dropping // condition) when no changes happen. return true; @@ -643,10 +603,6 @@ public class ZenModeHelper { */ void applyGlobalZenModeAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid, int zenMode) { - if (!android.app.Flags.modesApi()) { - Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!"); - return; - } synchronized (mConfigLock) { ZenModeConfig config = getConfigLocked(user); if (config == null) { @@ -712,10 +668,6 @@ public class ZenModeHelper { */ void applyGlobalPolicyAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid, NotificationManager.Policy policy) { - if (!android.app.Flags.modesApi()) { - Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!"); - return; - } synchronized (mConfigLock) { ZenModeConfig config = getConfigLocked(user); if (config == null) { @@ -772,10 +724,6 @@ public class ZenModeHelper { */ @Nullable Policy getNotificationPolicyFromImplicitZenRule(UserHandle user, String callingPkg) { - if (!android.app.Flags.modesApi()) { - Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!"); - return getNotificationPolicy(user); - } synchronized (mConfigLock) { ZenModeConfig config = getConfigLocked(user); if (config == null) { @@ -814,7 +762,6 @@ public class ZenModeHelper { .appendPath(pkg) .build(); rule.enabled = true; - rule.modified = false; rule.component = null; rule.configurationActivity = null; return rule; @@ -918,15 +865,12 @@ public class ZenModeHelper { private void maybePreserveRemovedRule(ZenModeConfig config, ZenRule ruleToRemove, @ConfigOrigin int origin) { - if (!Flags.modesApi()) { - return; - } // If an app deletes a previously customized rule, keep it around to preserve // the user's customization when/if it's recreated later. // We don't try to preserve system-owned rules because their conditionIds (used as // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot // delete a system-owned rule. - if (origin == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp() + if (origin == ORIGIN_APP && ruleToRemove.isUserModified() && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) { String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove); if (deletedKey != null) { @@ -952,7 +896,7 @@ public class ZenModeHelper { if (rule == null || !canManageAutomaticZenRule(rule, callingUid)) { return Condition.STATE_UNKNOWN; } - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { return rule.isActive() ? STATE_TRUE : STATE_FALSE; } else { // Buggy, does not consider snoozing! @@ -971,16 +915,9 @@ public class ZenModeHelper { newConfig = config.copy(); ZenRule rule = newConfig.automaticRules.get(id); - if (Flags.modesApi()) { - if (rule != null && canManageAutomaticZenRule(rule, callingUid)) { - setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule), - condition, origin, "setAzrState: " + rule.id, callingUid); - } - } else { - ArrayList<ZenRule> rules = new ArrayList<>(); - rules.add(rule); // rule may be null and throw NPE in the next method. - setAutomaticZenRuleStateLocked(newConfig, rules, condition, origin, - "setAzrState: " + (rule != null ? rule.id : "null!"), callingUid); + if (rule != null && canManageAutomaticZenRule(rule, callingUid)) { + setAutomaticZenRuleStateLocked(newConfig, Collections.singletonList(rule), + condition, origin, "setAzrState: " + rule.id, callingUid); } } } @@ -995,13 +932,12 @@ public class ZenModeHelper { newConfig = config.copy(); List<ZenRule> matchingRules = findMatchingRules(newConfig, ruleConditionId, condition); - if (Flags.modesApi()) { - for (int i = matchingRules.size() - 1; i >= 0; i--) { - if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) { - matchingRules.remove(i); - } + for (int i = matchingRules.size() - 1; i >= 0; i--) { + if (!canManageAutomaticZenRule(matchingRules.get(i), callingUid)) { + matchingRules.remove(i); } } + setAutomaticZenRuleStateLocked(newConfig, matchingRules, condition, origin, "setAzrStateFromCps: " + ruleConditionId, callingUid); } @@ -1013,7 +949,7 @@ public class ZenModeHelper { if (rules == null || rules.isEmpty()) return; if (!Flags.modesUi()) { - if (Flags.modesApi() && condition.source == SOURCE_USER_ACTION) { + if (condition.source == SOURCE_USER_ACTION) { origin = ORIGIN_USER_IN_APP; // Although coming from app, it's actually from user. } } @@ -1026,7 +962,7 @@ public class ZenModeHelper { private static void applyConditionAndReconsiderOverride(ZenRule rule, Condition condition, int origin) { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { if (isImplicitRuleId(rule.id)) { // Implicit rules do not use overrides, and always apply conditions directly. // This is compatible with the previous behavior (where the package set the @@ -1173,8 +1109,7 @@ public class ZenModeHelper { // if default rule wasn't user-modified use localized name // instead of previous system name if (currRule != null - && !currRule.modified - && (currRule.zenPolicyUserModifiedFields & AutomaticZenRule.FIELD_NAME) == 0 + && (currRule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0 && !defaultRule.name.equals(currRule.name)) { if (DEBUG) { Slog.d(TAG, "Locale change - updating default zen rule name " @@ -1184,7 +1119,7 @@ public class ZenModeHelper { updated = true; } } - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { for (ZenRule rule : newConfig.automaticRules.values()) { if (SystemZenRules.isSystemOwnedRule(rule)) { updated |= SystemZenRules.updateTriggerDescription(mContext, rule); @@ -1256,172 +1191,145 @@ public class ZenModeHelper { @GuardedBy("mConfigLock") private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenModeConfig config, ZenRule rule, @ConfigOrigin int origin, boolean isNew) { - if (Flags.modesApi()) { - boolean modified = false; - // These values can always be edited by the app, so we apply changes immediately. - if (isNew) { - rule.id = ZenModeConfig.newRuleId(); - rule.creationTime = mClock.millis(); - rule.component = azr.getOwner(); - rule.pkg = pkg; - modified = true; - } - // Allow updating the CPS backing system rules (e.g. for custom manual -> schedule) - if (Flags.modesUi() - && (origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI) - && Objects.equals(rule.pkg, SystemZenRules.PACKAGE_ANDROID) - && !Objects.equals(rule.component, azr.getOwner())) { - rule.component = azr.getOwner(); - modified = true; - } + boolean modified = false; + // These values can always be edited by the app, so we apply changes immediately. + if (isNew) { + rule.id = ZenModeConfig.newRuleId(); + rule.creationTime = mClock.millis(); + rule.component = azr.getOwner(); + rule.pkg = pkg; + modified = true; + } - if (Flags.modesUi()) { - if (!azr.isEnabled() && (isNew || rule.enabled)) { - // Creating a rule as disabled, or disabling a previously enabled rule. - // Record whodunit. - rule.disabledOrigin = origin; - } else if (azr.isEnabled()) { - // Enabling or previously enabled. Clear disabler. - rule.disabledOrigin = ORIGIN_UNKNOWN; - } - } + // Allow updating the CPS backing system rules (e.g. for custom manual -> schedule) + if (Flags.modesUi() + && (origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI) + && Objects.equals(rule.pkg, SystemZenRules.PACKAGE_ANDROID) + && !Objects.equals(rule.component, azr.getOwner())) { + rule.component = azr.getOwner(); + modified = true; + } - if (!Objects.equals(rule.conditionId, azr.getConditionId())) { - rule.conditionId = azr.getConditionId(); - modified = true; - } - // This can be removed when {@link Flags#modesUi} is fully ramped up - final boolean isWatch = - mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); - boolean shouldPreserveCondition = - Flags.modesApi() - && (Flags.modesUi() || isWatch) - && !isNew - && origin == ORIGIN_USER_IN_SYSTEMUI - && rule.enabled == azr.isEnabled() - && rule.conditionId != null - && rule.condition != null - && rule.conditionId.equals(rule.condition.id); - if (!shouldPreserveCondition) { - // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR. - rule.condition = null; - } - - if (rule.enabled != azr.isEnabled()) { - rule.enabled = azr.isEnabled(); - rule.resetConditionOverride(); - modified = true; - } - if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) { - rule.configurationActivity = azr.getConfigurationActivity(); - modified = true; - } - if (rule.allowManualInvocation != azr.isManualInvocationAllowed()) { - rule.allowManualInvocation = azr.isManualInvocationAllowed(); - modified = true; - } - if (!Flags.modesUi()) { - String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId()); - if (!Objects.equals(rule.iconResName, iconResName)) { - rule.iconResName = iconResName; - modified = true; - } - } - if (!Objects.equals(rule.triggerDescription, azr.getTriggerDescription())) { - rule.triggerDescription = azr.getTriggerDescription(); - modified = true; + if (Flags.modesUi()) { + if (!azr.isEnabled() && (isNew || rule.enabled)) { + // Creating a rule as disabled, or disabling a previously enabled rule. + // Record whodunit. + rule.disabledOrigin = origin; + } else if (azr.isEnabled()) { + // Enabling or previously enabled. Clear disabler. + rule.disabledOrigin = ORIGIN_UNKNOWN; } - if (rule.type != azr.getType()) { - rule.type = azr.getType(); + } + + if (!Objects.equals(rule.conditionId, azr.getConditionId())) { + rule.conditionId = azr.getConditionId(); + modified = true; + } + // This can be removed when {@link Flags#modesUi} is fully ramped up + final boolean isWatch = + mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); + boolean shouldPreserveCondition = + (Flags.modesUi() || isWatch) + && !isNew + && origin == ORIGIN_USER_IN_SYSTEMUI + && rule.enabled == azr.isEnabled() + && rule.conditionId != null + && rule.condition != null + && rule.conditionId.equals(rule.condition.id); + if (!shouldPreserveCondition) { + // Do not update 'modified'. If only this changes we treat it as a no-op updateAZR. + rule.condition = null; + } + + if (rule.enabled != azr.isEnabled()) { + rule.enabled = azr.isEnabled(); + rule.resetConditionOverride(); + modified = true; + } + if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) { + rule.configurationActivity = azr.getConfigurationActivity(); + modified = true; + } + if (rule.allowManualInvocation != azr.isManualInvocationAllowed()) { + rule.allowManualInvocation = azr.isManualInvocationAllowed(); + modified = true; + } + if (!Flags.modesUi()) { + String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId()); + if (!Objects.equals(rule.iconResName, iconResName)) { + rule.iconResName = iconResName; modified = true; } - // TODO: b/310620812 - Remove this once FLAG_MODES_API is inlined. - rule.modified = azr.isModified(); + } + if (!Objects.equals(rule.triggerDescription, azr.getTriggerDescription())) { + rule.triggerDescription = azr.getTriggerDescription(); + modified = true; + } + if (rule.type != azr.getType()) { + rule.type = azr.getType(); + modified = true; + } - // Name is treated differently than other values: - // App is allowed to update name if the name was not modified by the user (even if - // other values have been modified). In this way, if the locale of an app changes, - // i18n of the rule name can still occur even if the user has customized the rule - // contents. - String previousName = rule.name; - if (isNew || doesOriginAlwaysUpdateValues(origin) - || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) { - rule.name = azr.getName(); - modified |= !Objects.equals(rule.name, previousName); - } + // Name is treated differently than other values: + // App is allowed to update name if the name was not modified by the user (even if + // other values have been modified). In this way, if the locale of an app changes, + // i18n of the rule name can still occur even if the user has customized the rule + // contents. + String previousName = rule.name; + if (isNew || doesOriginAlwaysUpdateValues(origin) + || (rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) { + rule.name = azr.getName(); + modified |= !Objects.equals(rule.name, previousName); + } - // For the remaining values, rules can always have all values updated if: - // * the rule is newly added, or - // * the request comes from an origin that can always update values, like the user, or - // * the rule has not yet been user modified, and thus can be updated by the app. - boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin) - || rule.canBeUpdatedByApp(); + // For the remaining values, rules can always have all values updated if: + // * the rule is newly added, or + // * the request comes from an origin that can always update values, like the user, or + // * the rule has not yet been user modified, and thus can be updated by the app. + boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin) + || !rule.isUserModified(); - // For all other values, if updates are not allowed, we discard the update. - if (!updateValues) { - return modified; - } + // For all other values, if updates are not allowed, we discard the update. + if (!updateValues) { + return modified; + } - // Updates the bitmasks if the origin of the change is the user. - boolean updateBitmask = (origin == ORIGIN_USER_IN_SYSTEMUI); + // Updates the bitmasks if the origin of the change is the user. + boolean updateBitmask = (origin == ORIGIN_USER_IN_SYSTEMUI); - if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) { - rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; + if (updateBitmask && !TextUtils.equals(previousName, azr.getName())) { + rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; + } + int newZenMode = NotificationManager.zenModeFromInterruptionFilter( + azr.getInterruptionFilter(), Global.ZEN_MODE_OFF); + if (rule.zenMode != newZenMode) { + rule.zenMode = newZenMode; + if (updateBitmask) { + rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER; } - int newZenMode = NotificationManager.zenModeFromInterruptionFilter( - azr.getInterruptionFilter(), Global.ZEN_MODE_OFF); - if (rule.zenMode != newZenMode) { - rule.zenMode = newZenMode; + modified = true; + } + + if (Flags.modesUi()) { + String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId()); + if (!Objects.equals(rule.iconResName, iconResName)) { + rule.iconResName = iconResName; if (updateBitmask) { - rule.userModifiedFields |= AutomaticZenRule.FIELD_INTERRUPTION_FILTER; + rule.userModifiedFields |= AutomaticZenRule.FIELD_ICON; } modified = true; } + } - if (Flags.modesUi()) { - String iconResName = drawableResIdToResName(rule.pkg, azr.getIconResId()); - if (!Objects.equals(rule.iconResName, iconResName)) { - rule.iconResName = iconResName; - if (updateBitmask) { - rule.userModifiedFields |= AutomaticZenRule.FIELD_ICON; - } - modified = true; - } - } - - // Updates the bitmask and values for all policy fields, based on the origin. - modified |= updatePolicy(config, rule, azr.getZenPolicy(), updateBitmask, isNew); + // Updates the bitmask and values for all policy fields, based on the origin. + modified |= updatePolicy(config, rule, azr.getZenPolicy(), updateBitmask, isNew); - // Updates the bitmask and values for all device effect fields, based on the origin. - modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(), - origin == ORIGIN_APP, updateBitmask); + // Updates the bitmask and values for all device effect fields, based on the origin. + modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(), + origin == ORIGIN_APP, updateBitmask); - return modified; - } else { - if (rule.enabled != azr.isEnabled()) { - rule.resetConditionOverride(); - } - rule.name = azr.getName(); - rule.condition = null; - rule.conditionId = azr.getConditionId(); - rule.enabled = azr.isEnabled(); - rule.modified = azr.isModified(); - rule.zenPolicy = azr.getZenPolicy(); - rule.zenMode = NotificationManager.zenModeFromInterruptionFilter( - azr.getInterruptionFilter(), Global.ZEN_MODE_OFF); - rule.configurationActivity = azr.getConfigurationActivity(); - - if (isNew) { - rule.id = ZenModeConfig.newRuleId(); - rule.creationTime = System.currentTimeMillis(); - rule.component = azr.getOwner(); - rule.pkg = pkg; - } - - // Only the MODES_API path cares about the result, so just return whatever here. - return true; - } + return modified; } /** @@ -1629,32 +1537,21 @@ public class ZenModeHelper { } private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) { - AutomaticZenRule azr; - if (Flags.modesApi()) { - azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId) - .setManualInvocationAllowed(rule.allowManualInvocation) - .setPackage(rule.pkg) - .setCreationTime(rule.creationTime) - .setIconResId(drawableResNameToResId(rule.pkg, rule.iconResName)) - .setType(rule.type) - .setZenPolicy(rule.zenPolicy) - .setDeviceEffects(rule.zenDeviceEffects) - .setEnabled(rule.enabled) - .setInterruptionFilter( - NotificationManager.zenModeToInterruptionFilter(rule.zenMode)) - .setOwner(rule.component) - .setConfigurationActivity(rule.configurationActivity) - .setTriggerDescription(rule.triggerDescription) - .build(); - } else { - azr = new AutomaticZenRule(rule.name, rule.component, - rule.configurationActivity, - rule.conditionId, rule.zenPolicy, - NotificationManager.zenModeToInterruptionFilter(rule.zenMode), - rule.enabled, rule.creationTime); - azr.setPackageName(rule.pkg); - } - return azr; + return new AutomaticZenRule.Builder(rule.name, rule.conditionId) + .setManualInvocationAllowed(rule.allowManualInvocation) + .setPackage(rule.pkg) + .setCreationTime(rule.creationTime) + .setIconResId(drawableResNameToResId(rule.pkg, rule.iconResName)) + .setType(rule.type) + .setZenPolicy(rule.zenPolicy) + .setDeviceEffects(rule.zenDeviceEffects) + .setEnabled(rule.enabled) + .setInterruptionFilter( + NotificationManager.zenModeToInterruptionFilter(rule.zenMode)) + .setOwner(rule.component) + .setConfigurationActivity(rule.configurationActivity) + .setTriggerDescription(rule.triggerDescription) + .build(); } // Update only the hasPriorityChannels state (aka areChannelsBypassingDnd) without modifying @@ -1669,12 +1566,12 @@ public class ZenModeHelper { if (config == null) return; // If it already matches, do nothing - if (config.areChannelsBypassingDnd == hasPriorityChannels) { + if (config.hasPriorityChannels == hasPriorityChannels) { return; } ZenModeConfig newConfig = config.copy(); - newConfig.areChannelsBypassingDnd = hasPriorityChannels; + newConfig.hasPriorityChannels = hasPriorityChannels; // The updated calculation of whether there are priority channels is always done by // the system, even if the event causing the calculation had a different origin. setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "updateHasPriorityChannels", @@ -1754,9 +1651,7 @@ public class ZenModeHelper { newRule.zenMode = zenMode; newRule.conditionId = conditionId; newRule.enabler = caller; - if (Flags.modesApi()) { - newRule.allowManualInvocation = true; - } + newRule.allowManualInvocation = true; newConfig.manualRule = newRule; } } @@ -1849,7 +1744,7 @@ public class ZenModeHelper { boolean hasDefaultRules = config.automaticRules.containsAll( ZenModeConfig.getDefaultRuleIds()); - long time = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis(); + long time = mClock.millis(); if (config.automaticRules != null && config.automaticRules.size() > 0) { for (ZenRule automaticRule : config.automaticRules.values()) { if (forRestore) { @@ -1863,7 +1758,7 @@ public class ZenModeHelper { // Upon upgrading to a version with modes_api enabled, keep all behaviors of // rules with null ZenPolicies explicitly as a copy of the global policy. - if (Flags.modesApi() && config.version < ZenModeConfig.XML_VERSION_MODES_API) { + if (config.version < ZenModeConfig.XML_VERSION_MODES_API) { // Keep the manual ("global") policy that from config. ZenPolicy manualRulePolicy = config.getZenPolicy(); if (automaticRule.zenPolicy == null) { @@ -1877,8 +1772,7 @@ public class ZenModeHelper { } } - if (Flags.modesApi() && Flags.modesUi() - && config.version < ZenModeConfig.XML_VERSION_MODES_UI) { + if (Flags.modesUi() && config.version < ZenModeConfig.XML_VERSION_MODES_UI) { // Clear icons from implicit rules. App icons are not suitable for some // surfaces, so juse use a default (the user can select a different one). if (ZenModeConfig.isImplicitRuleId(automaticRule.id)) { @@ -1904,11 +1798,11 @@ public class ZenModeHelper { reason += ", reset to default rules"; } - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { SystemZenRules.maybeUpgradeRules(mContext, config); } - if (Flags.modesApi() && forRestore) { + if (forRestore) { // Note: forBackup doesn't write deletedRules, but just in case. config.deletedRules.clear(); } @@ -1995,7 +1889,7 @@ public class ZenModeHelper { if (config == null) return; final ZenModeConfig newConfig = config.copy(); - if (Flags.modesApi() && !Flags.modesUi()) { + if (!Flags.modesUi()) { // Fix for b/337193321 -- propagate changes to notificationPolicy to rules where // the user cannot edit zen policy to emulate the previous "inheritance". ZenPolicy previousPolicy = ZenAdapters.notificationPolicyToZenPolicy( @@ -2026,6 +1920,7 @@ public class ZenModeHelper { * <ul> * <li>Rule instances whose owner is not installed. * <li>Deleted rules that were deleted more than 30 days ago. + * <li>Implicit rules that haven't been used in 30 days (and have not been customized). * </ul> */ private void cleanUpZenRules() { @@ -2034,17 +1929,20 @@ public class ZenModeHelper { final ZenModeConfig newConfig = mConfig.copy(); deleteRulesWithoutOwner(newConfig.automaticRules); - if (Flags.modesApi()) { - deleteRulesWithoutOwner(newConfig.deletedRules); - for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) { - ZenRule deletedRule = newConfig.deletedRules.valueAt(i); - if (deletedRule.deletionInstant == null - || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) { - newConfig.deletedRules.removeAt(i); - } + deleteRulesWithoutOwner(newConfig.deletedRules); + + for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) { + ZenRule deletedRule = newConfig.deletedRules.valueAt(i); + if (deletedRule.deletionInstant == null + || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) { + newConfig.deletedRules.removeAt(i); } } + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + deleteUnusedImplicitRules(newConfig.automaticRules); + } + if (!newConfig.equals(mConfig)) { setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "cleanUpZenRules", Process.SYSTEM_UID); @@ -2053,7 +1951,7 @@ public class ZenModeHelper { } private void deleteRulesWithoutOwner(ArrayMap<String, ZenRule> ruleList) { - long currentTime = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis(); + long currentTime = mClock.millis(); if (ruleList != null) { for (int i = ruleList.size() - 1; i >= 0; i--) { ZenRule rule = ruleList.valueAt(i); @@ -2070,6 +1968,29 @@ public class ZenModeHelper { } } + private void deleteUnusedImplicitRules(ArrayMap<String, ZenRule> ruleList) { + if (ruleList == null) { + return; + } + Instant deleteIfUnusedSince = mClock.instant().minus(IMPLICIT_RULE_KEPT_FOR); + + for (int i = ruleList.size() - 1; i >= 0; i--) { + ZenRule rule = ruleList.valueAt(i); + if (isImplicitRuleId(rule.id) && !rule.isUserModified()) { + if (rule.lastActivation == null) { + // This rule existed before we started tracking activation time. It *might* be + // in use. Set lastActivation=now so it has some time (IMPLICIT_RULE_KEPT_FOR) + // before being removed if truly unused. + rule.lastActivation = mClock.instant(); + } + + if (rule.lastActivation.isBefore(deleteIfUnusedSince)) { + ruleList.removeAt(i); + } + } + } + } + /** * @return a copy of the zen mode configuration */ @@ -2188,7 +2109,7 @@ public class ZenModeHelper { mZenMode, mConfig, mConsolidatedPolicy); if (!config.equals(mConfig)) { // Schedule broadcasts. Cannot be sent during boot, though. - if (Flags.modesApi() && origin != ORIGIN_INIT) { + if (origin != ORIGIN_INIT) { for (ZenRule rule : config.automaticRules.values()) { ZenRule original = mConfig.automaticRules.get(rule.id); if (original != null) { @@ -2204,6 +2125,20 @@ public class ZenModeHelper { } } + // Update last activation for rules that are being activated. + if (Flags.modesUi() && Flags.modesCleanupImplicit()) { + Instant now = mClock.instant(); + if (!mConfig.isManualActive() && config.isManualActive()) { + config.manualRule.lastActivation = now; + } + for (ZenRule rule : config.automaticRules.values()) { + ZenRule previousRule = mConfig.automaticRules.get(rule.id); + if (rule.isActive() && (previousRule == null || !previousRule.isActive())) { + rule.lastActivation = now; + } + } + } + mConfig = config; dispatchOnConfigChanged(); updateAndApplyConsolidatedPolicyAndDeviceEffects(origin, reason); @@ -2295,7 +2230,7 @@ public class ZenModeHelper { private void applyCustomPolicy(ZenModeConfig config, ZenPolicy policy, ZenRule rule, boolean useManualConfig) { if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { policy.apply(ZenPolicy.getBasePolicyInterruptionFilterNone()); } else { policy.apply(new ZenPolicy.Builder() @@ -2304,7 +2239,7 @@ public class ZenModeHelper { .build()); } } else if (rule.zenMode == Global.ZEN_MODE_ALARMS) { - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { policy.apply(ZenPolicy.getBasePolicyInterruptionFilterAlarms()); } else { policy.apply(new ZenPolicy.Builder() @@ -2317,22 +2252,17 @@ public class ZenModeHelper { } else if (rule.zenPolicy != null) { policy.apply(rule.zenPolicy); } else { - if (Flags.modesApi()) { - if (useManualConfig) { - // manual rule is configured using the settings stored directly in ZenModeConfig - policy.apply(config.getZenPolicy()); - } else { - // under modes_api flag, an active automatic rule with no specified policy - // inherits the device default settings as stored in mDefaultConfig. While the - // rule's policy fields should be set upon creation, this is a fallback to - // catch any that may have fallen through the cracks. - Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule); - policy.apply(Flags.modesUi() - ? mDefaultConfig.getZenPolicy() : config.getZenPolicy()); - } - } else { - // active rule with no specified policy inherits the manual rule config settings + if (useManualConfig) { + // manual rule is configured using the settings stored directly in ZenModeConfig policy.apply(config.getZenPolicy()); + } else { + // An active automatic rule with no specified policy inherits the device default + // settings as stored in mDefaultConfig. While the rule's policy fields should be + // set upon creation, this is a fallback to catch any that may have fallen through + // the cracks. + Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule); + policy.apply(Flags.modesUi() + ? mDefaultConfig.getZenPolicy() : config.getZenPolicy()); } } } @@ -2346,9 +2276,7 @@ public class ZenModeHelper { ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder(); if (mConfig.isManualActive()) { applyCustomPolicy(mConfig, policy, mConfig.manualRule, true); - if (Flags.modesApi()) { - deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects); - } + deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects); } for (ZenRule automaticRule : mConfig.automaticRules.values()) { @@ -2356,12 +2284,10 @@ public class ZenModeHelper { // Active rules with INTERRUPTION_FILTER_ALL are not included in consolidated // policy. This is relevant in case some other active rule has a more // restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy! - if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) { + if (automaticRule.zenMode != Global.ZEN_MODE_OFF) { applyCustomPolicy(mConfig, policy, automaticRule, false); } - if (Flags.modesApi()) { - deviceEffectsBuilder.add(automaticRule.zenDeviceEffects); - } + deviceEffectsBuilder.add(automaticRule.zenDeviceEffects); } } @@ -2380,40 +2306,35 @@ public class ZenModeHelper { ZenLog.traceSetConsolidatedZenPolicy(mConsolidatedPolicy, reason); } - if (Flags.modesApi()) { - // Prevent other rules from applying grayscale if Driving is active (but allow it - // if _Driving itself_ wants grayscale). - if (Flags.modesUi() && preventZenDeviceEffectsWhileDriving()) { - boolean hasActiveDriving = false; - boolean hasActiveDrivingWithGrayscale = false; - for (ZenRule rule : mConfig.automaticRules.values()) { - if (rule.isActive() && rule.type == TYPE_DRIVING) { - hasActiveDriving = true; - if (rule.zenDeviceEffects != null - && rule.zenDeviceEffects.shouldDisplayGrayscale()) { - hasActiveDrivingWithGrayscale = true; - break; // Further rules won't affect decision. - } + // Prevent other rules from applying grayscale if Driving is active (but allow it + // if _Driving itself_ wants grayscale). + if (Flags.modesUi() && preventZenDeviceEffectsWhileDriving()) { + boolean hasActiveDriving = false; + boolean hasActiveDrivingWithGrayscale = false; + for (ZenRule rule : mConfig.automaticRules.values()) { + if (rule.isActive() && rule.type == TYPE_DRIVING) { + hasActiveDriving = true; + if (rule.zenDeviceEffects != null + && rule.zenDeviceEffects.shouldDisplayGrayscale()) { + hasActiveDrivingWithGrayscale = true; + break; // Further rules won't affect decision. } } - if (hasActiveDriving && !hasActiveDrivingWithGrayscale) { - deviceEffectsBuilder.setShouldDisplayGrayscale(false); - } } - - ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build(); - if (!deviceEffects.equals(mConsolidatedDeviceEffects)) { - mConsolidatedDeviceEffects = deviceEffects; - mHandler.postApplyDeviceEffects(origin); + if (hasActiveDriving && !hasActiveDrivingWithGrayscale) { + deviceEffectsBuilder.setShouldDisplayGrayscale(false); } } + + ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build(); + if (!deviceEffects.equals(mConsolidatedDeviceEffects)) { + mConsolidatedDeviceEffects = deviceEffects; + mHandler.postApplyDeviceEffects(origin); + } } } private void applyConsolidatedDeviceEffects(@ConfigOrigin int source) { - if (!Flags.modesApi()) { - return; - } DeviceEffectsApplier applier; ZenDeviceEffects effects; synchronized (mConfigLock) { @@ -2434,10 +2355,8 @@ public class ZenModeHelper { * to the current locale. */ private static void updateDefaultConfig(Context context, ZenModeConfig defaultConfig) { - if (Flags.modesApi()) { - updateDefaultAutomaticRulePolicies(defaultConfig); - } - if (Flags.modesApi() && Flags.modesUi()) { + updateDefaultAutomaticRulePolicies(defaultConfig); + if (Flags.modesUi()) { SystemZenRules.maybeUpgradeRules(context, defaultConfig); } updateRuleStringsForCurrentLocale(context, defaultConfig); @@ -2453,7 +2372,7 @@ public class ZenModeHelper { rule.name = context.getResources() .getString(R.string.zen_mode_default_every_night_name); } - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { SystemZenRules.updateTriggerDescription(context, rule); } } @@ -2462,10 +2381,6 @@ public class ZenModeHelper { // Updates the policies in the default automatic rules (provided via default XML config) to // be fully filled in default values. private static void updateDefaultAutomaticRulePolicies(ZenModeConfig defaultConfig) { - if (!Flags.modesApi()) { - // Should be checked before calling, but just in case. - return; - } ZenPolicy defaultPolicy = defaultConfig.getZenPolicy(); for (ZenRule rule : defaultConfig.automaticRules.values()) { if (ZenModeConfig.getDefaultRuleIds().contains(rule.id) && rule.zenPolicy == null) { @@ -2611,6 +2526,7 @@ public class ZenModeHelper { } } + // TODO: b/368247671 - Delete this method AND default_zen_mode_config.xml when inlining modes_ui private ZenModeConfig readDefaultConfig(Resources resources) { XmlResourceParser parser = null; try { @@ -2649,7 +2565,7 @@ public class ZenModeHelper { events.add(FrameworkStatsLog.buildStatsEvent(DND_MODE_RULE, /* optional int32 user = 1 */ user, /* optional bool enabled = 2 */ config.isManualActive(), - /* optional bool channels_bypassing = 3 */ config.areChannelsBypassingDnd, + /* optional bool channels_bypassing = 3 */ config.hasPriorityChannels, /* optional LoggedZenMode zen_mode = 4 */ ROOT_CONFIG, /* optional string id = 5 */ "", // empty for root config /* optional int32 uid = 6 */ Process.SYSTEM_UID, // system owns root config @@ -2924,9 +2840,6 @@ public class ZenModeHelper { * ({@link #addAutomaticZenRule}, {@link #removeAutomaticZenRule}, etc, makes sense. */ private static void checkManageRuleOrigin(String method, @ConfigOrigin int origin) { - if (!Flags.modesApi()) { - return; - } checkArgument(origin == ORIGIN_APP || origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI, "Expected one of ORIGIN_APP, ORIGIN_SYSTEM, or " @@ -2939,9 +2852,6 @@ public class ZenModeHelper { * {@link #setAutomaticZenRuleStateFromConditionProvider} makes sense. */ private static void checkSetRuleStateOrigin(String method, @ConfigOrigin int origin) { - if (!Flags.modesApi()) { - return; - } checkArgument(origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP || origin == ORIGIN_SYSTEM || origin == ORIGIN_USER_IN_SYSTEMUI, "Expected one of ORIGIN_APP, ORIGIN_USER_IN_APP, ORIGIN_SYSTEM, or " diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 048f2b6b0cbc..76cd5c88b388 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -210,3 +210,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "managed_services_concurrent_multiuser" + namespace: "systemui" + description: "Enables ManagedServices to support Concurrent multi user environment" + bug: "380297485" +} diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index d538bb876b64..c3af578de369 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -176,16 +176,13 @@ public class BackgroundInstallControlService extends SystemService { if (Flags.bicClient()) { mService.enforceCallerPermissions(); } - if (!Build.IS_DEBUGGABLE) { - return mService.getBackgroundInstalledPackages(flags, userId); - } // The debug.transparency.bg-install-apps (only works for debuggable builds) // is used to set mock list of background installed apps for testing. // The list of apps' names is delimited by ",". // TODO: Remove after migrating test to new background install method using // {@link BackgroundInstallControlCallbackHelperTest}.installPackage b/310983905 String propertyString = SystemProperties.get("debug.transparency.bg-install-apps"); - if (TextUtils.isEmpty(propertyString)) { + if (TextUtils.isEmpty(propertyString) || !Build.IS_DEBUGGABLE) { return mService.getBackgroundInstalledPackages(flags, userId); } else { return mService.getMockBackgroundInstalledPackages(propertyString); @@ -219,10 +216,27 @@ public class BackgroundInstallControlService extends SystemService { PackageManager.PackageInfoFlags.of(flags), userId); initBackgroundInstalledPackages(); + if(Build.IS_DEBUGGABLE) { + StringBuilder sb = new StringBuilder(); + sb.append("Tracked background installed package size: ") + .append(mBackgroundInstalledPackages.size()) + .append("\n"); + for (int i = 0; i < mBackgroundInstalledPackages.size(); ++i) { + int installingUserId = mBackgroundInstalledPackages.keyAt(i); + mBackgroundInstalledPackages.get(installingUserId).forEach(pkgName -> + sb.append("userId: ").append(installingUserId) + .append(", name: ").append(pkgName).append("\n")); + } + Slog.d(TAG, "Tracked background installed package: " + sb.toString()); + } + ListIterator<PackageInfo> iter = packages.listIterator(); while (iter.hasNext()) { String packageName = iter.next().packageName; if (!mBackgroundInstalledPackages.contains(userId, packageName)) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, packageName + " is not tracked, removing"); + } iter.remove(); } } @@ -284,6 +298,9 @@ public class BackgroundInstallControlService extends SystemService { } void handlePackageAdd(String packageName, int userId) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, "handlePackageAdd: checking " + packageName); + } ApplicationInfo appInfo = null; try { appInfo = @@ -302,7 +319,7 @@ public class BackgroundInstallControlService extends SystemService { installerPackageName = installInfo.getInstallingPackageName(); initiatingPackageName = installInfo.getInitiatingPackageName(); } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Package's installer not found " + packageName); + Slog.w(TAG, "Package's installer not found: " + packageName); return; } @@ -314,6 +331,10 @@ public class BackgroundInstallControlService extends SystemService { VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT, userId) != PERMISSION_GRANTED) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, "handlePackageAdd " + packageName + ": installer doesn't " + + "have INSTALL_PACKAGES permission, skipping"); + } return; } @@ -324,6 +345,10 @@ public class BackgroundInstallControlService extends SystemService { if (installedByAdb(initiatingPackageName) || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) { + if(Build.IS_DEBUGGABLE) { + Slog.d(TAG, "handlePackageAdd " + packageName + ": is installed by ADB or was " + + "foreground installation, skipping"); + } return; } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 4153cd1be0a6..76c5240ab623 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1163,15 +1163,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - private boolean shouldShowHub() { - final boolean hubEnabled = Settings.Secure.getIntForUser( - mContext.getContentResolver(), Settings.Secure.GLANCEABLE_HUB_ENABLED, - 1, mCurrentUserId) == 1; - - return mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled - && mDreamManagerInternal.dreamConditionActive(); - } - @VisibleForTesting void powerPress(long eventTime, int count, int displayId) { // SideFPS still needs to know about suppressed power buttons, in case it needs to block @@ -1270,10 +1261,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { // show hub. boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled( mCurrentUserId); - if (shouldShowHub() && keyguardAvailable) { - // If the hub can be launched, send a message to keyguard. We do not know if - // the hub is already running or not, keyguard handles turning screen off if - // it is. + if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled + && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) { + // If the hub can be launched, send a message to keyguard. Bundle options = new Bundle(); options.putBoolean(EXTRA_TRIGGER_HUB, true); lockNow(options); @@ -1334,14 +1324,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { * @param isScreenOn Whether the screen is currently on. * @param noDreamAction The action to perform if dreaming is not possible. */ - private boolean attemptToDreamFromShortPowerButtonPress( + private void attemptToDreamFromShortPowerButtonPress( boolean isScreenOn, Runnable noDreamAction) { if (mShortPressOnPowerBehavior != SHORT_PRESS_POWER_DREAM_OR_SLEEP && mShortPressOnPowerBehavior != SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP) { // If the power button behavior isn't one that should be able to trigger the dream, give // up. noDreamAction.run(); - return false; + return; } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); @@ -1349,7 +1339,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Slog.d(TAG, "Can't start dreaming when attempting to dream from short power" + " press (isScreenOn=" + isScreenOn + ")"); noDreamAction.run(); - return false; + return; } synchronized (mLock) { @@ -1360,8 +1350,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } dreamManagerInternal.requestDream(); - - return true; } /** @@ -6410,17 +6398,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { event.getDisplayId(), event.getKeyCode(), "wakeUpFromWakeKey")) { return; } - - if (!shouldShowHub() - && mShortPressOnPowerBehavior == SHORT_PRESS_POWER_HUB_OR_DREAM_OR_SLEEP - && event.getKeyCode() == KEYCODE_POWER - && attemptToDreamFromShortPowerButtonPress(false, () -> {})) { - // In the case that we should wake to dream and successfully initiate dreaming, do not - // continue waking up. Doing so will exit the dream state and cause UI to react - // accordingly. - return; - } - wakeUpFromWakeKey( event.getEventTime(), event.getKeyCode(), diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 8fae875eb29b..e3eced252d1f 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -3379,7 +3379,7 @@ public final class PowerManagerService extends SystemService } changed = sleepPowerGroupLocked(powerGroup, time, PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE, Process.SYSTEM_UID); - } else if (shouldNapAtBedTimeLocked()) { + } else if (shouldNapAtBedTimeLocked(powerGroup)) { changed = dreamPowerGroupLocked(powerGroup, time, Process.SYSTEM_UID, /* allowWake= */ false); } else { @@ -3395,7 +3395,10 @@ public final class PowerManagerService extends SystemService * activity timeout has expired and it's bedtime. */ @GuardedBy("mLock") - private boolean shouldNapAtBedTimeLocked() { + private boolean shouldNapAtBedTimeLocked(PowerGroup powerGroup) { + if (!powerGroup.supportsSandmanLocked()) { + return false; + } return mDreamsActivateOnSleepSetting || (mDreamsActivateOnDockSetting && mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) @@ -3617,9 +3620,10 @@ public final class PowerManagerService extends SystemService if (!mDreamsDisabledByAmbientModeSuppressionConfig) { return; } + final PowerGroup defaultPowerGroup = mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP); if (!isSuppressed && mIsPowered && mDreamsSupportedConfig && mDreamsEnabledSetting - && shouldNapAtBedTimeLocked() && isItBedTimeYetLocked( - mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP))) { + && shouldNapAtBedTimeLocked(defaultPowerGroup) + && isItBedTimeYetLocked(defaultPowerGroup)) { napInternal(SystemClock.uptimeMillis(), Process.SYSTEM_UID, /* allowWake= */ true); } else if (isSuppressed) { mDirty |= DIRTY_SETTINGS; diff --git a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java index a75d110e3cd1..17739712d65a 100644 --- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java +++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java @@ -88,6 +88,5 @@ public class ResourcesManagerShellCommand extends ShellCommand { out.println(" Print this help text."); out.println(" dump <PROCESS>"); out.println(" Dump the Resources objects in use as well as the history of Resources"); - } } diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java index f060e4d11e82..82df310db9a4 100644 --- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java +++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java @@ -303,7 +303,11 @@ class AttestationVerificationPeerDeviceVerifier { if (mRevocationEnabled) { // Checks Revocation Status List based on // https://developer.android.com/training/articles/security-key-attestation#certificate_status - mCertificateRevocationStatusManager.checkRevocationStatus(certificates); + // The first certificate is the leaf, which is generated at runtime with the attestation + // attributes such as the challenge. It is specific to this attestation instance and + // does not need to be checked for revocation. + mCertificateRevocationStatusManager.checkRevocationStatus( + new ArrayList<>(certificates.subList(1, certificates.size()))); } } diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java index d36d9f5f6636..4cd4b3b84910 100644 --- a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java +++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java @@ -42,6 +42,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.security.cert.CertPathValidatorException; import java.security.cert.X509Certificate; +import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; @@ -67,6 +68,8 @@ class CertificateRevocationStatusManager { */ @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30; + @VisibleForTesting static final int NUM_HOURS_BEFORE_NEXT_CHECK = 24; + /** * The number of days since issue date for an intermediary certificate to be considered fresh * and not require a revocation list check. @@ -127,6 +130,17 @@ class CertificateRevocationStatusManager { serialNumbers.add(serialNumber); } try { + if (isLastCheckedWithin(Duration.ofHours(NUM_HOURS_BEFORE_NEXT_CHECK), serialNumbers)) { + Slog.d( + TAG, + "All certificates have been checked for revocation recently. No need to" + + " check this time."); + return; + } + } catch (IOException ignored) { + // Proceed to check the revocation status + } + try { JSONObject revocationList = fetchRemoteRevocationList(); Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); for (String serialNumber : serialNumbers) { @@ -151,25 +165,32 @@ class CertificateRevocationStatusManager { serialNumbers.remove(serialNumber); } } - Map<String, LocalDateTime> lastRevocationCheckData; try { - lastRevocationCheckData = getLastRevocationCheckData(); + if (!isLastCheckedWithin( + Duration.ofDays(MAX_DAYS_SINCE_LAST_CHECK), serialNumbers)) { + throw new CertPathValidatorException( + "Unable to verify the revocation status of one of the certificates " + + serialNumbers); + } } catch (IOException ex2) { throw new CertPathValidatorException( "Unable to load stored revocation status", ex2); } - for (String serialNumber : serialNumbers) { - if (!lastRevocationCheckData.containsKey(serialNumber) - || lastRevocationCheckData - .get(serialNumber) - .isBefore( - LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) { - throw new CertPathValidatorException( - "Unable to verify the revocation status of certificate " - + serialNumber); - } + } + } + + private boolean isLastCheckedWithin(Duration lastCheckedWithin, List<String> serialNumbers) + throws IOException { + Map<String, LocalDateTime> lastRevocationCheckData = getLastRevocationCheckData(); + for (String serialNumber : serialNumbers) { + if (!lastRevocationCheckData.containsKey(serialNumber) + || lastRevocationCheckData + .get(serialNumber) + .isBefore(LocalDateTime.now().minus(lastCheckedWithin))) { + return false; } } + return true; } private static boolean needToCheckRevocationStatus( diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java index bfd86d724583..9f9a9807d973 100644 --- a/services/core/java/com/android/server/security/FileIntegrityService.java +++ b/services/core/java/com/android/server/security/FileIntegrityService.java @@ -54,11 +54,6 @@ public class FileIntegrityService extends SystemService { super(PermissionEnforcer.fromContext(context)); } - @Override - public boolean isApkVeritySupported() { - return VerityUtils.isFsVeritySupported(); - } - private void checkCallerPackageName(String packageName) { final int callingUid = Binder.getCallingUid(); final int callingUserId = UserHandle.getUserId(callingUid); diff --git a/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java index 687442b47fb3..cdeacaa2e43a 100644 --- a/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java +++ b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java @@ -62,7 +62,7 @@ public class DataAggregator { /** Initialize DataSources */ private void initialize() { mDataSources.add(new SecurityLogSource(mContext, this)); - mDataSources.add(new NetworkLogSource(mContext, this)); + mDataSources.add(new NetworkLogSource(this)); } /** diff --git a/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java b/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java index f303a588d30c..fe0cf80a48f2 100644 --- a/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java +++ b/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java @@ -18,7 +18,6 @@ package com.android.server.security.intrusiondetection; import android.app.admin.ConnectEvent; import android.app.admin.DnsEvent; -import android.content.Context; import android.content.pm.PackageManagerInternal; import android.net.IIpConnectivityMetrics; import android.net.INetdEventCallback; @@ -44,8 +43,7 @@ public class NetworkLogSource implements DataSource { private IIpConnectivityMetrics mIpConnectivityMetrics; private long mId; - public NetworkLogSource(Context context, DataAggregator dataAggregator) - throws SecurityException { + public NetworkLogSource(DataAggregator dataAggregator) throws SecurityException { mDataAggregator = dataAggregator; mPm = LocalServices.getService(PackageManagerInternal.class); mId = 0; diff --git a/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java index 142094c9d9f4..7501799198e8 100644 --- a/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java +++ b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java @@ -19,14 +19,15 @@ package com.android.server.security.intrusiondetection; import android.Manifest.permission; import android.annotation.RequiresPermission; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.SecurityLog.SecurityEvent; import android.content.Context; import android.security.intrusiondetection.IntrusionDetectionEvent; import android.util.Slog; +import com.android.server.LocalServices; + import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -36,13 +37,13 @@ public class SecurityLogSource implements DataSource { private SecurityEventCallback mEventCallback; private DevicePolicyManager mDpm; - private Executor mExecutor; + private DevicePolicyManagerInternal mDpmInternal; private DataAggregator mDataAggregator; public SecurityLogSource(Context context, DataAggregator dataAggregator) { mDataAggregator = dataAggregator; mDpm = context.getSystemService(DevicePolicyManager.class); - mExecutor = Executors.newSingleThreadExecutor(); + mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class); mEventCallback = new SecurityEventCallback(); } @@ -50,12 +51,13 @@ public class SecurityLogSource implements DataSource { @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void enable() { enableAuditLog(); - mDpm.setAuditLogEventCallback(mExecutor, mEventCallback); + mDpmInternal.setInternalEventsCallback(mEventCallback); } @Override @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void disable() { + mDpmInternal.setInternalEventsCallback(null); disableAuditLog(); } @@ -82,10 +84,11 @@ public class SecurityLogSource implements DataSource { @Override public void accept(List<SecurityEvent> events) { - if (events.size() == 0) { + if (events == null || events.size() == 0) { Slog.w(TAG, "No events received; caller may not be authorized"); return; } + List<IntrusionDetectionEvent> intrusionDetectionEvents = events.stream() .filter(event -> event != null) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 03fe7775edb0..c37b5a055140 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8051,6 +8051,7 @@ final class ActivityRecord extends WindowToken { mConfigurationSeq = Math.max(++mConfigurationSeq, 1); getResolvedOverrideConfiguration().seq = mConfigurationSeq; + // TODO(b/392069771): Move to AppCompatSandboxingPolicy. // Sandbox max bounds by setting it to the activity bounds, if activity is letterboxed, or // has or will have mAppCompatDisplayInsets for size compat. Also forces an activity to be // sandboxed or not depending upon the configuration settings. @@ -8079,6 +8080,9 @@ final class ActivityRecord extends WindowToken { resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); } + mAppCompatController.getSandboxingPolicy().sandboxBoundsIfNeeded(resolvedConfig, + parentWindowingMode); + applySizeOverrideIfNeeded( mDisplayContent, info.applicationInfo, diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 6a5adca91e39..b607b0fce9ab 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -145,6 +145,7 @@ import android.util.SparseIntArray; import android.view.Display; import android.webkit.URLUtil; import android.window.ActivityWindowInfo; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.R; @@ -2916,6 +2917,8 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { /** The helper to calculate whether a container is opaque. */ static class OpaqueContainerHelper implements Predicate<ActivityRecord> { + private final boolean mEnableMultipleDesktopsBackend = + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue(); private ActivityRecord mStarting; private boolean mIgnoringInvisibleActivity; private boolean mIgnoringKeyguard; @@ -2938,7 +2941,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { mIgnoringKeyguard = ignoringKeyguard; final boolean isOpaque; - if (!Flags.enableMultipleDesktopsBackend()) { + if (!mEnableMultipleDesktopsBackend) { isOpaque = container.getActivity(this, true /* traverseTopToBottom */, null /* boundary */) != null; } else { @@ -2949,13 +2952,16 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } private boolean isOpaqueInner(@NonNull WindowContainer<?> container) { - // If it's a leaf task fragment, then opacity is calculated based on its activities. - if (container.asTaskFragment() != null - && ((TaskFragment) container).isLeafTaskFragment()) { + final boolean isActivity = container.asActivityRecord() != null; + final boolean isLeafTaskFragment = container.asTaskFragment() != null + && ((TaskFragment) container).isLeafTaskFragment(); + if (isActivity || isLeafTaskFragment) { + // When it is an activity or leaf task fragment, then opacity is calculated based + // on itself or its activities. return container.getActivity(this, true /* traverseTopToBottom */, null /* boundary */) != null; } - // When not a leaf, it's considered opaque if any of its opaque children fill this + // Otherwise, it's considered opaque if any of its opaque children fill this // container, unless the children are adjacent fragments, in which case as long as they // are all opaque then |container| is also considered opaque, even if the adjacent // task fragment aren't filling. diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index bed95face1c9..fc504796b0ac 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -44,6 +44,8 @@ class AppCompatController { private final AppCompatLetterboxPolicy mLetterboxPolicy; @NonNull private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy; + @NonNull + private final AppCompatSandboxingPolicy mSandboxingPolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -66,6 +68,7 @@ class AppCompatController { mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration); mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord, mAppCompatOverrides); + mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord); } @NonNull @@ -143,6 +146,11 @@ class AppCompatController { return mSizeCompatModePolicy; } + @NonNull + AppCompatSandboxingPolicy getSandboxingPolicy() { + return mSandboxingPolicy; + } + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getLetterboxPolicy().dump(pw, prefix); diff --git a/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java new file mode 100644 index 000000000000..26cf32b12d4f --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import static com.android.server.wm.AppCompatUtils.isInDesktopMode; + +import android.annotation.NonNull; +import android.app.WindowConfiguration.WindowingMode; +import android.content.res.Configuration; +import android.graphics.Rect; + +import com.android.window.flags.Flags; + +/** + * Encapsulate logic related to sandboxing for app compatibility. + */ +class AppCompatSandboxingPolicy { + + @NonNull + private final ActivityRecord mActivityRecord; + + AppCompatSandboxingPolicy(@NonNull ActivityRecord activityRecord) { + mActivityRecord = activityRecord; + } + + /** + * In freeform, the container bounds are scaled with app bounds. Activity bounds can be + * outside of its container bounds if insets are coupled with configuration outside of + * freeform and maintained in freeform for size compat mode. + * + * <p>Sandbox activity bounds in freeform to app bounds to force app to display within the + * container. This prevents UI cropping when activities can draw below insets which are + * normally excluded from appBounds before targetSDK < 35 + * (see ConfigurationContainer#applySizeOverrideIfNeeded). + */ + void sandboxBoundsIfNeeded(@NonNull Configuration resolvedConfig, + @WindowingMode int windowingMode) { + if (!Flags.excludeCaptionFromAppBounds()) { + return; + } + + if (isInDesktopMode(mActivityRecord.mAtmService.mContext, windowingMode)) { + Rect appBounds = resolvedConfig.windowConfiguration.getAppBounds(); + if (appBounds == null || appBounds.isEmpty()) { + // When there is no override bounds, the activity will inherit the bounds from + // parent. + appBounds = mActivityRecord.mResolveConfigHint.mParentAppBoundsOverride; + } + resolvedConfig.windowConfiguration.setBounds(appBounds); + } + } +} diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index bbc33004ee54..2cfa242bc5fe 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -17,14 +17,13 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_METADATA; import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_OVERRIDE; import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_METADATA; import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_OVERRIDE; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; -import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode; +import static com.android.server.wm.AppCompatUtils.isInDesktopMode; import android.annotation.NonNull; import android.annotation.Nullable; @@ -545,9 +544,8 @@ class AppCompatSizeCompatModePolicy { // Allow an application to be up-scaled if its window is smaller than its // original container or if it's a freeform window in desktop mode. boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH) - || (canEnterDesktopMode(mActivityRecord.mAtmService.mContext) - && newParentConfig.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_FREEFORM); + || isInDesktopMode(mActivityRecord.mAtmService.mContext, + newParentConfig.windowConfiguration.getWindowingMode()); return shouldAllowUpscaling ? Math.min( (float) viewportW / contentW, (float) viewportH / contentH) : 1f; } diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 3e054fc40540..146044008b3f 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -16,16 +16,20 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.content.res.Configuration.UI_MODE_TYPE_MASK; import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET; import static com.android.server.wm.ActivityRecord.State.RESUMED; +import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppCompatTaskInfo; import android.app.CameraCompatTaskInfo; import android.app.TaskInfo; +import android.app.WindowConfiguration.WindowingMode; +import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.view.InsetsSource; @@ -276,6 +280,14 @@ final class AppCompatUtils { inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY); } + /** + * Return {@code true} if window is currently in desktop mode. + */ + static boolean isInDesktopMode(@NonNull Context context, + @WindowingMode int parentWindowingMode) { + return parentWindowingMode == WINDOWING_MODE_FREEFORM && canEnterDesktopMode(context); + } + private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) { info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e76a83453a9d..094ad187686c 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -190,7 +190,9 @@ class BackNavigationController { currentActivity = window.mActivityRecord; currentTask = window.getTask(); if ((currentTask != null && !currentTask.isVisibleRequested()) - || (currentActivity != null && !currentActivity.isVisibleRequested())) { + || (currentActivity != null && !currentActivity.isVisibleRequested()) + || (currentActivity != null && currentTask != null + && currentTask.getTopNonFinishingActivity() != currentActivity)) { // Closing transition is happening on focus window and should be update soon, // don't drive back navigation with it. ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focus window is closing."); diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index 4eaa11bac016..f473b7b7e4fb 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -60,10 +60,11 @@ class DeferredDisplayUpdater { */ @VisibleForTesting static final DisplayInfoFieldsUpdater DEFERRABLE_FIELDS = (out, override) -> { - // Treat unique id and address change as WM-specific display change as we re-query display - // settings and parameters based on it which could cause window changes + // Treat unique id, address, and canHostTasks change as WM-specific display change as we + // re-query display settings and parameters based on it which could cause window changes. out.uniqueId = override.uniqueId; out.address = override.address; + out.canHostTasks = override.canHostTasks; // Also apply WM-override fields, since they might produce differences in window hierarchy WM_OVERRIDE_FIELDS.setFields(out, override); @@ -433,7 +434,7 @@ class DeferredDisplayUpdater { second.thermalRefreshRateThrottling) || !Objects.equals(first.thermalBrightnessThrottlingDataId, second.thermalBrightnessThrottlingDataId) - || first.canHostTasks != second.canHostTasks) { + ) { diff |= DIFF_NOT_WM_DEFERRABLE; } @@ -454,6 +455,7 @@ class DeferredDisplayUpdater { || !Objects.equals(first.displayShape, second.displayShape) || !Objects.equals(first.uniqueId, second.uniqueId) || !Objects.equals(first.address, second.address) + || first.canHostTasks != second.canHostTasks ) { diff |= DIFF_WM_DEFERRABLE; } diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index f35930700653..c2255d8d011a 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -51,13 +51,8 @@ public final class DesktopModeHelper { } /** - * Return {@code true} if the current device can hosts desktop sessions on its internal display. + * Return {@code true} if the current device supports desktop mode. */ - @VisibleForTesting - static boolean canInternalDisplayHostDesktops(@NonNull Context context) { - return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); - } - // TODO(b/337819319): use a companion object instead. private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); @@ -68,32 +63,45 @@ public final class DesktopModeHelper { } /** + * Return {@code true} if the current device can hosts desktop sessions on its internal display. + */ + @VisibleForTesting + static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + + /** * Check if Desktop mode should be enabled because the dev option is shown and enabled. */ private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported( - context) || isInternalDisplayEligibleToHostDesktops(context)); + context) || isDeviceEligibleForDesktopMode(context)); } @VisibleForTesting - static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { - return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( - Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported( - context)); + static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { + if (!shouldEnforceDeviceRestrictions()) { + return true; + } + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); + final boolean desktopModeSupportedByDevOptions = + Flags.enableDesktopModeThroughDevOption() + && isDesktopModeDevOptionsSupported(context); + return desktopModeSupported || desktopModeSupportedByDevOptions; } /** * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return (isInternalDisplayEligibleToHostDesktops(context) - && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() - && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions())) + return (isDeviceEligibleForDesktopMode(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) || isDesktopModeEnabledByDevOption(context); } /** Returns {@code true} if desktop experience wallpaper is supported on this device. */ public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) { - return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context); + return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context); } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 682f3d8cf1e5..703ce7d24468 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3239,25 +3239,43 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp Slog.e(TAG, "ShouldShowSystemDecors shouldn't be updated when the flag is off."); } - final boolean shouldShow; - if (isDefaultDisplay) { - shouldShow = true; - } else if (isPrivate()) { - shouldShow = false; - } else { - shouldShow = mDisplay.canHostTasks(); + final boolean shouldShowContent; + if (!allowContentModeSwitch()) { + return; } + shouldShowContent = mDisplay.canHostTasks(); - if (shouldShow == mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) { + if (shouldShowContent == mWmService.mDisplayWindowSettings + .shouldShowSystemDecorsLocked(this)) { return; } - mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShow); + mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShowContent); - if (!shouldShow) { + if (!shouldShowContent) { clearAllTasksOnDisplay(null /* clearTasksCallback */, false /* isRemovingDisplay */); } } + private boolean allowContentModeSwitch() { + // The default display should always show system decorations. + if (isDefaultDisplay) { + return false; + } + + // Private display should never show system decorations. + if (isPrivate()) { + return false; + } + + // TODO(b/391965805): Remove this after introducing FLAG_ALLOW_SYSTEM_DECORATIONS_CHANGE. + // Virtual displays cannot add or remove system decorations during their lifecycle. + if (mDisplay.getType() == Display.TYPE_VIRTUAL) { + return false; + } + + return true; + } + DisplayCutout loadDisplayCutout(int displayWidth, int displayHeight) { if (mDisplayPolicy == null || mInitialDisplayCutout == null) { return null; @@ -6578,22 +6596,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getKeyguardController().isKeyguardLocked(mDisplayId); } - boolean isKeyguardLockedOrAodShowing() { - return isKeyguardLocked() || isAodShowing(); - } - - /** - * @return whether aod is showing for this display - */ - boolean isAodShowing() { - final boolean isAodShowing = mRootWindowContainer.mTaskSupervisor - .getKeyguardController().isAodShowing(mDisplayId); - if (mDisplayId == DEFAULT_DISPLAY && isAodShowing) { - return !isKeyguardGoingAway(); - } - return isAodShowing; - } - /** * @return whether keyguard is going away on this display */ diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 69f32cb7b8ea..84281b8fbecf 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -122,7 +122,7 @@ class DragState { float mThumbOffsetX, mThumbOffsetY; InputInterceptor mInputInterceptor; ArrayList<WindowState> mNotifiedWindows; - boolean mDragInProgress; + private boolean mDragInProgress; // Set to non -1 value if a valid app requests DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START int mCallingTaskIdToHide; /** @@ -161,7 +161,7 @@ class DragState { private boolean mIsClosing; // Stores the last drop event which was reported to a valid drop target window, or null - // otherwise. This drop event will contain private info and should only be consumed by the + // otherwise. This drop event will contain private info and should only be consumed by the // unhandled drag listener. DragEvent mUnhandledDropEvent; @@ -243,7 +243,7 @@ class DragState { for (WindowState ws : mNotifiedWindows) { float inWindowX = 0; float inWindowY = 0; - SurfaceControl dragSurface = null; + boolean includeDragSurface = false; if (!mDragResult && (ws.mSession.mPid == mPid)) { // Report unconsumed drop location back to the app that started the drag. inWindowX = ws.translateToWindowX(mCurrentDisplayX); @@ -251,13 +251,10 @@ class DragState { if (relinquishDragSurfaceToDragSource()) { // If requested (and allowed), report the drag surface back to the app // starting the drag to handle the return animation - dragSurface = mSurfaceControl; + includeDragSurface = true; } } - DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX, - inWindowY, mThumbOffsetX, mThumbOffsetY, - mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null, - dragSurface, null, mDragResult); + DragEvent event = obtainDragEndedEvent(inWindowX, inWindowY, includeDragSurface); try { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws); ws.mClient.dispatchDragEvent(event); @@ -310,10 +307,10 @@ class DragState { /** * Creates the drop event for dispatching to the unhandled drag. - * TODO(b/384841906): Update `inWindowX` and `inWindowY` to be display-coordinate. */ - private DragEvent createUnhandledDropEvent(float inWindowX, float inWindowY) { - return obtainDragEvent(DragEvent.ACTION_DROP, inWindowX, inWindowY, mDataDescription, mData, + private DragEvent createUnhandledDropEvent(float inDisplayX, float inDisplayY) { + return obtainDragEvent(DragEvent.ACTION_DROP, inDisplayX, inDisplayY, mDataDescription, + mData, /* includeDragSurface= */ true, /* includeDragFlags= */ true, null /* dragAndDropPermissions */); } @@ -370,11 +367,8 @@ class DragState { } final WindowState touchedWin = mService.mInputToWindowMap.get(token); - // TODO(b/384841906): The x, y here when sent to a window and unhandled, will still be - // relative to the window it was originally sent to. Need to update this to actually be - // display-coordinate. - final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY); if (!isWindowNotified(touchedWin)) { + final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY); // Delegate to the unhandled drag listener as a first pass if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) { // The unhandled drag listener will call back to notify whether it has consumed @@ -392,6 +386,8 @@ class DragState { } if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to " + touchedWin); + final DragEvent unhandledDropEvent = createUnhandledDropEvent( + touchedWin.getBounds().left + inWindowX, touchedWin.getBounds().top + inWindowY); final IBinder clientToken = touchedWin.mClient.asBinder(); final DragEvent event = createDropEvent(inWindowX, inWindowY, touchedWin); @@ -776,28 +772,37 @@ class DragState { displayId, (int) (displayX - mThumbOffsetX), (int) (displayY - mThumbOffsetY)); } - /** - * Returns true if it has sent DRAG_STARTED broadcast out but has not been sent DRAG_END - * broadcast. - */ - boolean isInProgress() { - return mDragInProgress; + private DragEvent obtainDragEndedEvent(float x, float y, boolean includeDragSurface) { + return obtainDragEvent(DragEvent.ACTION_DRAG_ENDED, x, y, /* description= */ + null, /* data= */ null, includeDragSurface, /* includeDragFlags= */ + true, /* dragAndDropPermissions= */ null, mDragResult); + } + + private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description, + ClipData data, boolean includeDragSurface, boolean includeDragFlags, + IDragAndDropPermissions dragAndDropPermissions) { + return obtainDragEvent(action, x, y, description, data, includeDragSurface, + includeDragFlags, dragAndDropPermissions, /* dragResult= */ false); } /** * `x` and `y` here varies between local window coordinate, relative coordinate to another * window and local display coordinate, all depending on the `action`. Please take a look * at the callers to determine the type. - * TODO(b/384845022): Properly document the events sent based on the event type. + * - ACTION_DRAG_STARTED: (x, y) is relative coordinate to the target window's origin + * (possible to have negative values). + * - ACTION_DROP: + * --- UnhandledDropEvent: (x, y) is in display space coordinate. + * --- DropEvent: (x, y) is in local window coordinate where event is targeted to. + * - ACTION_DRAG_ENDED: (x, y) is in local window coordinate where event is targeted to. */ private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description, ClipData data, boolean includeDragSurface, boolean includeDragFlags, - IDragAndDropPermissions dragAndDropPermissions) { + IDragAndDropPermissions dragAndDropPermissions, boolean dragResult) { return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY, mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0, null /* localState */, description, data, - includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, - false /* result */); + includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, dragResult); } private ValueAnimator createReturnAnimationLocked() { diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index dd2f49e171a8..6091b8334438 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -18,7 +18,6 @@ package com.android.server.wm; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION; @@ -217,9 +216,6 @@ class KeyguardController { } else if (keyguardShowing && !state.mKeyguardShowing) { transition.addFlag(TRANSIT_FLAG_KEYGUARD_APPEARING); } - if (mWindowManager.mFlags.mAodTransition && aodShowing && !state.mAodShowing) { - transition.addFlag(TRANSIT_FLAG_AOD_APPEARING); - } } } // Update the task snapshot if the screen will not be turned off. To make sure that the @@ -242,27 +238,19 @@ class KeyguardController { state.mAodShowing = aodShowing; state.writeEventLog("setKeyguardShown"); - if (keyguardChanged || aodChanged) { - if (keyguardChanged) { - // Irrelevant to AOD. - state.mKeyguardGoingAway = false; - if (keyguardShowing) { - state.mDismissalRequested = false; - } + if (keyguardChanged) { + // Irrelevant to AOD. + state.mKeyguardGoingAway = false; + if (keyguardShowing) { + state.mDismissalRequested = false; } if (goingAwayRemoved - || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state)) - || (mWindowManager.mFlags.mAodTransition && aodShowing)) { + || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))) { // Keyguard decided to show or stopped going away. Send a transition to animate back // to the locked state before holding the sleep token again if (!ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS) { dc.requestTransitionAndLegacyPrepare( TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING); - if (mWindowManager.mFlags.mAodTransition && aodShowing - && dc.mTransitionController.isCollecting()) { - dc.mTransitionController.getCollectingTransition().addFlag( - TRANSIT_FLAG_AOD_APPEARING); - } } dc.mWallpaperController.adjustWallpaperWindows(); dc.executeAppTransition(); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 3abab8bf62c2..bf9883c76a06 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -166,6 +166,7 @@ import android.view.InsetsState; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import android.window.ITaskOrganizer; import android.window.PictureInPictureSurfaceTransaction; @@ -2378,7 +2379,7 @@ class Task extends TaskFragment { // configurations and let its parent (organized task) to control it; final Task rootTask = getRootTask(); boolean shouldInheritBounds = rootTask != this && rootTask.isOrganized(); - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { // Only inherit from organized parent when this task is not organized. shouldInheritBounds &= !isOrganized(); } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index fe653e454d6c..5217a759c6ae 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -36,7 +36,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_OPEN; @@ -974,10 +973,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return false; } - boolean isInAodAppearTransition() { - return (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0; - } - /** * Specifies configuration change explicitly for the window container, so it can be chosen as * transition target. This is usually used with transition mode diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 25b513d85384..ba7f36419ac5 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -525,19 +525,6 @@ class TransitionController { return false; } - boolean isInAodAppearTransition() { - if (mCollectingTransition != null && mCollectingTransition.isInAodAppearTransition()) { - return true; - } - for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { - if (mWaitingTransitions.get(i).isInAodAppearTransition()) return true; - } - for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { - if (mPlayingTransitions.get(i).isInAodAppearTransition()) return true; - } - return false; - } - /** * @return A pair of the transition and restore-behind target for the given {@param container}. * @param container An ancestor of a transient-launch activity diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 70948e1264c4..c1ef208d1d4d 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -166,14 +166,6 @@ class WallpaperController { mFindResults.setWallpaperTarget(w); return false; } - } else if (mService.mFlags.mAodTransition - && mDisplayContent.isKeyguardLockedOrAodShowing()) { - if (mService.mPolicy.isKeyguardHostWindow(w.mAttrs) - && w.mTransitionController.isInAodAppearTransition()) { - if (DEBUG_WALLPAPER) Slog.v(TAG, "Found aod transition wallpaper target: " + w); - mFindResults.setWallpaperTarget(w); - return true; - } } final boolean animationWallpaper = animatingContainer != null @@ -692,8 +684,7 @@ class WallpaperController { private WallpaperWindowToken getTokenForTarget(WindowState target) { if (target == null) return null; WindowState window = mFindResults.getTopWallpaper( - (target.canShowWhenLocked() && mService.isKeyguardLocked()) - || (mService.mFlags.mAodTransition && mDisplayContent.isAodShowing())); + target.canShowWhenLocked() && mService.isKeyguardLocked()); return window == null ? null : window.mToken.asWallpaperToken(); } @@ -736,9 +727,7 @@ class WallpaperController { if (mFindResults.wallpaperTarget == null && mFindResults.useTopWallpaperAsTarget) { mFindResults.setWallpaperTarget( - mFindResults.getTopWallpaper(mService.mFlags.mAodTransition - ? mDisplayContent.isKeyguardLockedOrAodShowing() - : mDisplayContent.isKeyguardLocked())); + mFindResults.getTopWallpaper(mDisplayContent.isKeyguardLocked())); } } @@ -910,17 +899,11 @@ class WallpaperController { if (mDisplayContent.mWmService.mFlags.mEnsureWallpaperInTransitions) { visibleRequested = mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested(); } - updateWallpaperTokens(visibleRequested, - mService.mFlags.mAodTransition - ? mDisplayContent.isKeyguardLockedOrAodShowing() - : mDisplayContent.isKeyguardLocked()); + updateWallpaperTokens(visibleRequested, mDisplayContent.isKeyguardLocked()); ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper at display %d - visibility: %b, keyguardLocked: %b", - mDisplayContent.getDisplayId(), visible, - mService.mFlags.mAodTransition - ? mDisplayContent.isKeyguardLockedOrAodShowing() - : mDisplayContent.isKeyguardLocked()); + mDisplayContent.getDisplayId(), visible, mDisplayContent.isKeyguardLocked()); if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) { mLastFrozen = mFindResults.isWallpaperTargetForLetterbox; diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index f07e6722d836..0d0c0bad24fa 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -124,6 +124,7 @@ static struct { jmethodID notifyStylusGestureStarted; jmethodID notifyVibratorState; jmethodID filterInputEvent; + jmethodID filterPointerMotion; jmethodID interceptKeyBeforeQueueing; jmethodID interceptMotionBeforeQueueingNonInteractive; jmethodID interceptKeyBeforeDispatching; @@ -451,6 +452,8 @@ public: void notifyPointerDisplayIdChanged(ui::LogicalDisplayId displayId, const vec2& position) override; void notifyMouseCursorFadedOnTyping() override; + std::optional<vec2> filterPointerMotionForAccessibility( + const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) override; /* --- InputFilterPolicyInterface implementation --- */ void notifyStickyModifierStateChanged(uint32_t modifierState, @@ -938,6 +941,27 @@ void NativeInputManager::notifyStickyModifierStateChanged(uint32_t modifierState checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged"); } +std::optional<vec2> NativeInputManager::filterPointerMotionForAccessibility( + const vec2& current, const vec2& delta, const ui::LogicalDisplayId& displayId) { + JNIEnv* env = jniEnv(); + ScopedFloatArrayRO filtered(env, + jfloatArray( + env->CallObjectMethod(mServiceObj, + gServiceClassInfo.filterPointerMotion, + delta.x, delta.y, current.x, + current.y, displayId.val()))); + if (checkAndClearExceptionFromCallback(env, "filterPointerMotionForAccessibilityLocked")) { + ALOGE("Disabling accessibility pointer motion filter due to an error. " + "The filter state in Java and PointerChoreographer would no longer be in sync."); + return std::nullopt; + } + LOG_ALWAYS_FATAL_IF(filtered.size() != 2, + "Accessibility pointer motion filter is misbehaving. Returned array size " + "%zu should be 2.", + filtered.size()); + return vec2{filtered[0], filtered[1]}; +} + sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(ui::LogicalDisplayId displayId) { JNIEnv* env = jniEnv(); jlong nativeSurfaceControlPtr = @@ -3271,6 +3295,12 @@ static jboolean nativeSetKernelWakeEnabled(JNIEnv* env, jobject nativeImplObj, j return im->getInputManager()->getReader().setKernelWakeEnabled(deviceId, enabled); } +static void nativeSetAccessibilityPointerMotionFilterEnabled(JNIEnv* env, jobject nativeImplObj, + jboolean enabled) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->getInputManager()->getChoreographer().setAccessibilityPointerMotionFilterEnabled(enabled); +} + // ---------------------------------------------------------------------------- static const JNINativeMethod gInputManagerMethods[] = { @@ -3398,6 +3428,8 @@ static const JNINativeMethod gInputManagerMethods[] = { {"setInputMethodConnectionIsActive", "(Z)V", (void*)nativeSetInputMethodConnectionIsActive}, {"getLastUsedInputDeviceId", "()I", (void*)nativeGetLastUsedInputDeviceId}, {"setKernelWakeEnabled", "(IZ)Z", (void*)nativeSetKernelWakeEnabled}, + {"setAccessibilityPointerMotionFilterEnabled", "(Z)V", + (void*)nativeSetAccessibilityPointerMotionFilterEnabled}, }; #define FIND_CLASS(var, className) \ @@ -3482,6 +3514,8 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.filterInputEvent, clazz, "filterInputEvent", "(Landroid/view/InputEvent;I)Z"); + GET_METHOD_ID(gServiceClassInfo.filterPointerMotion, clazz, "filterPointerMotion", "(FFFFI)[F"); + GET_METHOD_ID(gServiceClassInfo.interceptKeyBeforeQueueing, clazz, "interceptKeyBeforeQueueing", "(Landroid/view/KeyEvent;I)I"); diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index a96c477c78d2..f731b50d81b4 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -17,6 +17,8 @@ package com.android.server.supervision; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_USERS; +import static android.Manifest.permission.QUERY_USERS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static com.android.internal.util.Preconditions.checkCallAuthorization; @@ -79,6 +81,25 @@ public class SupervisionService extends ISupervisionManager.Stub { } /** + * Creates an {@link Intent} that can be used with {@link Context#startActivity(Intent)} to + * launch the activity to verify supervision credentials. + * + * <p>A valid {@link Intent} is always returned if supervision is enabled at the time this + * method is called, the launched activity still need to perform validity checks as the + * supervision state can change when it's launched. A null intent is returned if supervision is + * disabled at the time of this method call. + * + * <p>A result code of {@link android.app.Activity#RESULT_OK} indicates successful verification + * of the supervision credentials. + */ + @Override + @Nullable + public Intent createConfirmSupervisionCredentialsIntent() { + // TODO(b/392961554): Implement createAuthenticationIntent API + throw new UnsupportedOperationException(); + } + + /** * Returns whether supervision is enabled for the given user. * * <p>Supervision is automatically enabled when the supervision app becomes the profile owner or @@ -86,6 +107,7 @@ public class SupervisionService extends ISupervisionManager.Stub { */ @Override public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { + enforceAnyPermission(QUERY_USERS, MANAGE_USERS); if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { enforcePermission(INTERACT_ACROSS_USERS); } @@ -96,6 +118,7 @@ public class SupervisionService extends ISupervisionManager.Stub { @Override public void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) { + // TODO(b/395630828): Ensure that this method can only be called by the system. if (UserHandle.getUserId(Binder.getCallingUid()) != userId) { enforcePermission(INTERACT_ACROSS_USERS); } @@ -181,8 +204,8 @@ public class SupervisionService extends ISupervisionManager.Stub { * Ensures that supervision is enabled when the supervision app is the profile owner. * * <p>The state syncing with the DevicePolicyManager can only enable supervision and never - * disable. Supervision can only be disabled explicitly via calls to the - * {@link #setSupervisionEnabledForUser} method. + * disable. Supervision can only be disabled explicitly via calls to the {@link + * #setSupervisionEnabledForUser} method. */ private void syncStateWithDevicePolicyManager(@UserIdInt int userId) { final DevicePolicyManagerInternal dpmInternal = mInjector.getDpmInternal(); @@ -221,6 +244,17 @@ public class SupervisionService extends ISupervisionManager.Stub { mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED); } + /** Enforces that the caller has at least one of the given permission. */ + private void enforceAnyPermission(String... permissions) { + boolean authorized = false; + for (String permission : permissions) { + if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + authorized = true; + } + } + checkCallAuthorization(authorized); + } + /** Provides local services in a lazy manner. */ static class Injector { private final Context mContext; @@ -280,7 +314,7 @@ public class SupervisionService extends ISupervisionManager.Stub { } @VisibleForTesting - @SuppressLint("MissingPermission") // not needed for a system service + @SuppressLint("MissingPermission") void registerProfileOwnerListener() { IntentFilter poIntentFilter = new IntentFilter(); poIntentFilter.addAction(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 5d64cb638702..2d3f7231cc5c 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -34,12 +34,12 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import android.app.ActivityManager; import android.app.Instrumentation; import android.content.res.Configuration; import android.graphics.Insets; +import android.os.Build; import android.os.RemoteException; -import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.server.wm.WindowManagerStateHelper; @@ -86,25 +86,36 @@ public class InputMethodServiceTest { "android:id/input_method_nav_back"; private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID = "android:id/input_method_nav_ime_switcher"; - private static final long TIMEOUT_IN_SECONDS = 3; - private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = - "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1"; - private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = - "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0"; + + /** Timeout until the uiObject should be found. */ + private static final long TIMEOUT_MS = 5000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Timeout until the event is expected. */ + private static final long EXPECT_TIMEOUT_MS = 3000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Timeout during which the event is not expected. */ + private static final long NOT_EXCEPT_TIMEOUT_MS = 2000L * Build.HW_TIMEOUT_MULTIPLIER; + + /** Command to set showing the IME when a hardware keyboard is connected. */ + private static final String SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD = + "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD; + /** Command to get verbose ImeTracker logging state. */ + private static final String GET_VERBOSE_IME_TRACKER_LOGGING_CMD = + "getprop persist.debug.imetracker"; + /** Command to set verbose ImeTracker logging state. */ + private static final String SET_VERBOSE_IME_TRACKER_LOGGING_CMD = + "setprop persist.debug.imetracker"; /** The ids of the subtypes of SimpleIme. */ private static final int[] SUBTYPE_IDS = new int[]{1, 2}; - private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); + private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); private final GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper(); private final DeviceFlagsValueProvider mFlagsValueProvider = new DeviceFlagsValueProvider(); @Rule - public final CheckFlagsRule mCheckFlagsRule = new CheckFlagsRule(mFlagsValueProvider); - - @Rule public final TestName mName = new TestName(); private Instrumentation mInstrumentation; @@ -114,7 +125,8 @@ public class InputMethodServiceTest { private String mInputMethodId; private TestActivity mActivity; private InputMethodServiceWrapper mInputMethodService; - private boolean mShowImeWithHardKeyboardEnabled; + private boolean mOriginalVerboseImeTrackerLoggingEnabled; + private boolean mOriginalShowImeWithHardKeyboardEnabled; @Before public void setUp() throws Exception { @@ -123,9 +135,12 @@ public class InputMethodServiceTest { mImm = mInstrumentation.getContext().getSystemService(InputMethodManager.class); mTargetPackageName = mInstrumentation.getTargetContext().getPackageName(); mInputMethodId = getInputMethodId(); + mOriginalVerboseImeTrackerLoggingEnabled = getVerboseImeTrackerLogging(); + if (!mOriginalVerboseImeTrackerLoggingEnabled) { + setVerboseImeTrackerLogging(true); + } prepareIme(); prepareActivity(); - mInstrumentation.waitForIdleSync(); mUiDevice.freezeRotation(); mUiDevice.setOrientationNatural(); // Waits for input binding ready. @@ -148,17 +163,18 @@ public class InputMethodServiceTest { .that(mInputMethodService.getCurrentInputViewStarted()).isFalse(); }); // Save the original value of show_ime_with_hard_keyboard from Settings. - mShowImeWithHardKeyboardEnabled = + mOriginalShowImeWithHardKeyboardEnabled = mInputMethodService.getShouldShowImeWithHardKeyboardForTesting(); } @After public void tearDown() throws Exception { mUiDevice.unfreezeRotation(); + if (!mOriginalVerboseImeTrackerLoggingEnabled) { + setVerboseImeTrackerLogging(false); + } // Change back the original value of show_ime_with_hard_keyboard in Settings. - executeShellCommand(mShowImeWithHardKeyboardEnabled - ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD - : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); + setShowImeWithHardKeyboard(mOriginalShowImeWithHardKeyboardEnabled); executeShellCommand("ime disable " + mInputMethodId); } @@ -170,7 +186,7 @@ public class InputMethodServiceTest { public void testShowHideKeyboard_byUserAction() { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Performs click on EditText to bring up the IME. Log.i(TAG, "Click on EditText"); @@ -201,14 +217,12 @@ public class InputMethodServiceTest { */ @Test public void testShowHideKeyboard_byInputMethodManager() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); @@ -219,14 +233,12 @@ public class InputMethodServiceTest { */ @Test public void testShowHideKeyboard_byInsetsController() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.hideImeWithWindowInsetsController(), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); @@ -234,53 +246,18 @@ public class InputMethodServiceTest { /** * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf. - * - * <p>With the refactor in b/298172246, all calls to IMMS#{show,hide}MySoftInputLocked - * will be just apply the requested visibility (by using the callback). Therefore, we will - * lose flags like HIDE_IMPLICIT_ONLY. */ @Test public void testShowHideSelf() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // IME request to show itself without any flags, expect shown. - Log.i(TAG, "Call IMS#requestShowSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestShowSelf(0 /* flags */), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown). - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf( - InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, false /* eventExpected */, true /* shown */, - "IME is still shown after HIDE_IMPLICIT_ONLY"); - } - - // IME request to hide itself without any flags, expect hidden. - Log.i(TAG, "Call IMS#requestHideSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestHideSelf(0 /* flags */), EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); - - if (!mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // IME request to show itself with flag SHOW_IMPLICIT, expect shown. - Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), - EVENT_SHOW, true /* eventExpected */, true /* shown */, - "IME is shown with SHOW_IMPLICIT"); - - // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden. - Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)"); - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.requestHideSelf( - InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, true /* eventExpected */, false /* shown */, - "IME is not shown after HIDE_IMPLICIT_ONLY"); - } } /** @@ -289,28 +266,25 @@ public class InputMethodServiceTest { */ @Test public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); try { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show with visible hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with visible hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_NOKEYS; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show without hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show without hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - eventually(() -> - assertWithMessage("InputView should show with hidden hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with hidden hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -323,28 +297,25 @@ public class InputMethodServiceTest { */ @Test public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); try { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should not show with visible hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isFalse()); + assertWithMessage("InputView should not show with visible hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isFalse(); config.keyboard = Configuration.KEYBOARD_NOKEYS; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO; - eventually(() -> - assertWithMessage("InputView should show without hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show without hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - eventually(() -> - assertWithMessage("InputView should show with hidden hardware keyboard") - .that(mInputMethodService.onEvaluateInputViewShown()).isTrue()); + assertWithMessage("InputView should show with hidden hardware keyboard") + .that(mInputMethodService.onEvaluateInputViewShown()).isTrue(); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -357,7 +328,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInput_disableShowImeWithHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -386,49 +357,17 @@ public class InputMethodServiceTest { } /** - * This checks that an explicit show request results in the IME being shown. - */ - @Test - public void testShowSoftInputExplicitly() { - setShowImeWithHardKeyboard(true /* enabled */); - - // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the - // IME should be shown. - verifyInputViewStatusOnMainSync( - () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - } - - /** - * This checks that an implicit show request results in the IME being shown. - */ - @Test - public void testShowSoftInputImplicitly() { - setShowImeWithHardKeyboard(true /* enabled */); - - // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT, - // the IME should be shown. - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - } - - /** * This checks that an explicit show request when the IME is not previously shown, * and it should be shown in fullscreen mode, results in the IME being shown. */ @Test public void testShowSoftInputExplicitly_fullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Set orientation landscape to enable fullscreen mode. setOrientation(2); - eventually(() -> assertWithMessage("No longer in natural orientation") - .that(mUiDevice.isNaturalOrientation()).isFalse()); - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. @@ -442,34 +381,40 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request when the IME is not previously shown, - * and it should be shown in fullscreen mode, results in the IME not being shown. + * and it should be shown in fullscreen mode behaves like an explicit show request, resulting + * in the IME being shown. This is due to the refactor in b/298172246, causing us to lose flag + * information like {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request when the IME is not previously shown, + * and it should be shown in fullscreen mode, would result in the IME not being shown. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_fullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); // Set orientation landscape to enable fullscreen mode. setOrientation(2); - eventually(() -> assertWithMessage("No longer in natural orientation") - .that(mUiDevice.isNaturalOrientation()).isFalse()); - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. eventually(() -> assertWithMessage("Has an input connection to the re-created Activity") .that(mImm.hasActiveInputConnection(mActivity.getEditText())).isTrue()); - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + } } /** @@ -478,7 +423,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -497,17 +442,17 @@ public class InputMethodServiceTest { } /** - * This checks that an implicit show request when a hardware keyboard is connected, - * results in the IME not being shown. + * This checks that an implicit show request when a hardware keyboard is connected behaves + * like an explicit show request, resulting in the IME being shown. This is due to the + * refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request when a hardware keyboard is connected would + * result in the IME not being shown. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -516,10 +461,20 @@ public class InputMethodServiceTest { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - verifyInputViewStatusOnMainSync(() ->assertThat( - mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)) - .isTrue(), - EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat( + mActivity.showImeWithInputMethodManager( + InputMethodManager.SHOW_IMPLICIT)) + .isTrue(), + EVENT_SHOW, false /* eventExpected */, false /* shown */, + "IME is not shown"); + } } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -532,7 +487,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_thenConfigurationChanged() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -565,17 +520,17 @@ public class InputMethodServiceTest { /** * This checks that an implicit show request followed by connecting a hardware keyboard - * and a configuration change, does not trigger IMS#onFinishInputView, - * but results in the IME being hidden. + * and a configuration change behaves like an explicit show request, resulting in the IME + * still being shown. This is due to the refactor in b/298172246, causing us to lose flag + * information like {@link InputMethodManager#SHOW_IMPLICIT}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * SHOW_IMPLICIT. + * <p>Previously, an implicit show request followed by connecting a hardware keyboard + * and a configuration change, would not trigger IMS#onFinishInputView, but resulted in the + * IME being hidden. */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputImplicitly_thenConfigurationChanged() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -596,16 +551,23 @@ public class InputMethodServiceTest { // Simulate a fake configuration change to avoid the recreation of TestActivity. config.orientation = Configuration.ORIENTATION_LANDSCAPE; - // Normally, IMS#onFinishInputView will be called when finishing the input view by - // the user. But if IMS#hideWindow is called when receiving a new configuration change, - // we don't expect that it's user-driven to finish the lifecycle of input view with - // IMS#onFinishInputView, because the input view will be re-initialized according - // to the last #mShowInputRequested state. So in this case we treat the input view as - // still alive. - verifyInputViewStatusOnMainSync( - () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */, - false /* shown */, "IME is not shown after a configuration change"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.onConfigurationChanged(config), + EVENT_CONFIG, true /* eventExpected */, true /* shown */, + "IME is still shown after a configuration change"); + } else { + // Normally, IMS#onFinishInputView will be called when finishing the input view by + // the user. But if IMS#hideWindow is called when receiving a new configuration + // change, we don't expect that it's user-driven to finish the lifecycle of input + // view with IMS#onFinishInputView, because the input view will be re-initialized + // according to the last #mShowInputRequested state. So in this case we treat the + // input view as still alive. + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.onConfigurationChanged(config), + EVENT_CONFIG, true /* eventExpected */, true /* inputViewStarted */, + false /* shown */, "IME is not shown after a configuration change"); + } } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -619,7 +581,7 @@ public class InputMethodServiceTest { */ @Test public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard() { - setShowImeWithHardKeyboard(false /* enabled */); + setShowImeWithHardKeyboard(false /* enable */); final var config = mInputMethodService.getResources().getConfiguration(); final var initialConfig = new Configuration(config); @@ -628,12 +590,10 @@ public class InputMethodServiceTest { config.keyboard = Configuration.KEYBOARD_QWERTY; config.hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES; - // Explicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); - // Implicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), @@ -654,17 +614,18 @@ public class InputMethodServiceTest { /** * This checks that a forced show request directly followed by an explicit show request, - * and then a hide not always request, still results in the IME being shown - * (i.e. the explicit show request retains the forced state). + * and then a not always hide request behaves like a normal hide request, resulting in the + * IME being hidden (i.e. the explicit show request does not retain the forced state). This is + * due to the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_FORCED}. * - * <p>With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput - * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like - * HIDE_NOT_ALWAYS. + * <p>Previously, a forced show request directly followed by an explicit show request, + * and then a not always hide request, would result in the IME still being shown + * (i.e. the explicit show request would retain the forced state). */ @Test - @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER) public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(), @@ -674,11 +635,123 @@ public class InputMethodServiceTest { mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown"); - verifyInputViewStatusOnMainSync(() -> assertThat( - mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) - .isTrue(), - EVENT_HIDE, false /* eventExpected */, true /* shown */, - "IME is still shown after HIDE_NOT_ALWAYS"); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync(() -> assertThat(mActivity + .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) + .isTrue(), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_NOT_ALWAYS"); + } else { + verifyInputViewStatusOnMainSync(() -> assertThat(mActivity + .hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) + .isTrue(), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_NOT_ALWAYS"); + } + } + + /** + * This checks that an explicit show request followed by an implicit only hide request + * behaves like a normal hide request, resulting in the IME being hidden. This is due to + * the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}. + * + * <p>Previously, an explicit show request followed by an implicit only hide request + * would result in the IME still being shown. + */ + @Test + public void testShowSoftInputExplicitly_thenHideSoftInputImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithInputMethodManager(0 /* flags */), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } else { + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_IMPLICIT_ONLY"); + } + } + + /** + * This checks that an implicit show request followed by an implicit only hide request + * results in the IME being hidden. + */ + @Test + public void testShowSoftInputImplicitly_thenHideSoftInputImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT), + EVENT_SHOW, true /* eventExpected */, true /* shown */, + "IME is shown with SHOW_IMPLICIT"); + + verifyInputViewStatusOnMainSync( + () -> mActivity.hideImeWithInputMethodManager( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } + + /** + * This checks that an explicit show self request followed by an implicit only hide self request + * behaves like a normal hide self request, resulting in the IME being hidden. This is due to + * the refactor in b/298172246, causing us to lose flag information like + * {@link InputMethodManager#SHOW_IMPLICIT} and {@link InputMethodManager#HIDE_IMPLICIT_ONLY}. + * + * <p>Previously, an explicit show self request followed by an implicit only hide self request + * would result in the IME still being shown. + */ + @Test + public void testShowSelfExplicitly_thenHideSelfImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestShowSelf(0 /* flags */), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); + } else { + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_IMPLICIT_ONLY"); + } + } + + /** + * This checks that an implicit show self request followed by an implicit only hide self request + * results in the IME being hidden. + */ + @Test + public void testShowSelfImplicitly_thenHideSelfImplicitOnly() { + setShowImeWithHardKeyboard(true /* enable */); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT), + EVENT_SHOW, true /* eventExpected */, true /* shown */, + "IME is shown with SHOW_IMPLICIT"); + + verifyInputViewStatusOnMainSync( + () -> mInputMethodService.requestHideSelf( + InputMethodManager.HIDE_IMPLICIT_ONLY), + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); } /** @@ -686,7 +759,7 @@ public class InputMethodServiceTest { */ @Test public void testFullScreenMode() { - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); Log.i(TAG, "Set orientation natural"); verifyFullscreenMode(() -> setOrientation(0), false /* eventExpected */, @@ -723,25 +796,22 @@ public class InputMethodServiceTest { public void testShowHideImeNavigationBar_doesDrawImeNavBar() { assumeTrue("Must have a navigation bar", hasNavigationBar()); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Show IME verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue(); - // Try to hide IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after hide request") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to show IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is shown after show request") @@ -758,25 +828,22 @@ public class InputMethodServiceTest { public void testShowHideImeNavigationBar_doesNotDrawImeNavBar() { assumeTrue("Must have a navigation bar", hasNavigationBar()); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); - // Show IME verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(false /* enabled */); + setDrawsImeNavBarAndSwitcherButton(false /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially not shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to hide IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(false /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after hide request") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); - // Try to show IME nav bar mInstrumentation.runOnMainSync(() -> setShowImeNavigationBar(true /* show */)); mInstrumentation.waitForIdleSync(); assertWithMessage("IME navigation bar is not shown after show request") @@ -792,7 +859,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( @@ -818,7 +885,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( @@ -844,7 +911,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var info = mImm.getCurrentInputMethodInfo(); assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); @@ -855,7 +922,7 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); @@ -884,7 +951,7 @@ public class InputMethodServiceTest { waitUntilActivityReadyForInputInjection(mActivity); - setShowImeWithHardKeyboard(true /* enabled */); + setShowImeWithHardKeyboard(true /* enable */); final var info = mImm.getCurrentInputMethodInfo(); assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); @@ -893,7 +960,7 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { - setDrawsImeNavBarAndSwitcherButton(true /* enabled */); + setDrawsImeNavBarAndSwitcherButton(true /* enable */); mActivity.showImeWithWindowInsetsController(); }, EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); @@ -956,7 +1023,8 @@ public class InputMethodServiceTest { runnable.run(); } mInstrumentation.waitForIdleSync(); - eventCalled = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + eventCalled = latch.await(eventExpected ? EXPECT_TIMEOUT_MS : NOT_EXCEPT_TIMEOUT_MS, + TimeUnit.MILLISECONDS); } catch (InterruptedException e) { fail("Interrupted while waiting for latch: " + e.getMessage()); return; @@ -1016,10 +1084,8 @@ public class InputMethodServiceTest { verifyInputViewStatus(runnable, EVENT_CONFIG, eventExpected, false /* shown */, "IME is not shown"); if (eventExpected) { - // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); - // Get the new TestActivity. mActivity = TestActivity.getInstance(); assertWithMessage("Re-created activity is not null").that(mActivity).isNotNull(); // Wait for the new EditText to be served by InputMethodManager. @@ -1062,6 +1128,7 @@ public class InputMethodServiceTest { private void prepareActivity() { mActivity = TestActivity.startSync(mInstrumentation); + mInstrumentation.waitForIdleSync(); Log.i(TAG, "Finish preparing activity with editor."); } @@ -1086,21 +1153,51 @@ public class InputMethodServiceTest { * @param enable the value to be set. */ private void setShowImeWithHardKeyboard(boolean enable) { + if (mInputMethodService == null) { + // If the IME is no longer around, reset the setting unconditionally. + executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0")); + return; + } + final boolean currentEnabled = mInputMethodService.getShouldShowImeWithHardKeyboardForTesting(); if (currentEnabled != enable) { - executeShellCommand(enable - ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD - : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD); + executeShellCommand(SET_SHOW_IME_WITH_HARD_KEYBOARD_CMD + " " + (enable ? "1" : "0")); eventually(() -> assertWithMessage("showImeWithHardKeyboard updated") .that(mInputMethodService.getShouldShowImeWithHardKeyboardForTesting()) .isEqualTo(enable)); } } - private static void executeShellCommand(@NonNull String cmd) { + /** + * Gets the verbose logging state in {@link android.view.inputmethod.ImeTracker}. + * + * @return {@code true} iff verbose logging is enabled. + */ + private static boolean getVerboseImeTrackerLogging() { + return executeShellCommand(GET_VERBOSE_IME_TRACKER_LOGGING_CMD).trim().equals("1"); + } + + /** + * Sets verbose logging in {@link android.view.inputmethod.ImeTracker}. + * + * @param enabled whether to enable or disable verbose logging. + * + * @implNote This must use {@link ActivityManager#notifySystemPropertiesChanged()} to listen + * for changes to the system property for the verbose ImeTracker logging. + */ + private void setVerboseImeTrackerLogging(boolean enabled) { + final var context = mInstrumentation.getContext(); + final var am = context.getSystemService(ActivityManager.class); + + executeShellCommand(SET_VERBOSE_IME_TRACKER_LOGGING_CMD + " " + (enabled ? "1" : "0")); + am.notifySystemPropertiesChanged(); + } + + @NonNull + private static String executeShellCommand(@NonNull String cmd) { Log.i(TAG, "Run command: " + cmd); - SystemUtil.runShellCommandOrThrow(cmd); + return SystemUtil.runShellCommandOrThrow(cmd); } /** @@ -1113,8 +1210,7 @@ public class InputMethodServiceTest { @NonNull private UiObject2 getUiObject(@NonNull BySelector bySelector) { - final var uiObject = mUiDevice.wait(Until.findObject(bySelector), - TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS)); + final var uiObject = mUiDevice.wait(Until.findObject(bySelector), TIMEOUT_MS); assertWithMessage("UiObject with " + bySelector + " was found").that(uiObject).isNotNull(); return uiObject; } @@ -1137,10 +1233,10 @@ public class InputMethodServiceTest { * * <p>Note, neither of these are normally drawn when in three button navigation mode. * - * @param enabled whether the IME nav bar and IME Switcher button are drawn. + * @param enable whether the IME nav bar and IME Switcher button are drawn. */ - private void setDrawsImeNavBarAndSwitcherButton(boolean enabled) { - final int flags = enabled ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; + private void setDrawsImeNavBarAndSwitcherButton(boolean enable) { + final int flags = enable ? IME_DRAWS_IME_NAV_BAR | SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(flags); } diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java index 558d1a7c4e8b..d4d4dcaa4f48 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/ims/InputMethodServiceWrapper.java @@ -111,12 +111,6 @@ public class InputMethodServiceWrapper extends InputMethodService { } @Override - public void requestHideSelf(int flags) { - Log.i(TAG, "requestHideSelf() " + flags); - super.requestHideSelf(flags); - } - - @Override public void onConfigurationChanged(Configuration newConfig) { Log.i(TAG, "onConfigurationChanged() " + newConfig); super.onConfigurationChanged(newConfig); diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java index f5c0de034483..e1c65d27459e 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java @@ -65,10 +65,13 @@ import android.content.pm.SigningDetails; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; import androidx.annotation.Nullable; @@ -150,6 +153,9 @@ public class PackageParserTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private File mTmpDir; private static final File FRAMEWORK = new File("/system/framework/framework-res.apk"); private static final String TEST_APP1_APK = "PackageParserTestApp1.apk"; @@ -846,7 +852,42 @@ public class PackageParserTest { @Test @RequiresFlagsEnabled(android.content.res.Flags.FLAG_MANIFEST_FLAGGING) - public void testParseWithFeatureFlagAttributes() throws Exception { + @DisableFlags(android.content.res.Flags.FLAG_USE_NEW_ACONFIG_STORAGE) + public void testParseWithFeatureFlagAttributes_oldStorage() throws Exception { + final File testFile = extractFile(TEST_APP8_APK); + try (PackageParser2 parser = new TestPackageParser2()) { + Map<String, Boolean> flagValues = new HashMap<>(); + flagValues.put("my.flag1", true); + flagValues.put("my.flag2", false); + flagValues.put("my.flag3", false); + flagValues.put("my.flag4", true); + ParsingPackageUtils.getAconfigFlags().addFlagValuesForTesting(flagValues); + + // The manifest has: + // <permission android:name="PERM1" android:featureFlag="my.flag1 " /> + // <permission android:name="PERM2" android:featureFlag=" !my.flag2" /> + // <permission android:name="PERM3" android:featureFlag="my.flag3" /> + // <permission android:name="PERM4" android:featureFlag="!my.flag4" /> + // <permission android:name="PERM5" android:featureFlag="unknown.flag" /> + // Therefore with the above flag values, only PERM1 and PERM2 should be present. + + final ParsedPackage pkg = parser.parsePackage(testFile, 0, false); + List<String> permissionNames = + pkg.getPermissions().stream().map(ParsedComponent::getName).toList(); + assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM1"); + assertThat(permissionNames).contains(PACKAGE_NAME + ".PERM2"); + assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM3"); + assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM4"); + assertThat(permissionNames).doesNotContain(PACKAGE_NAME + ".PERM5"); + } finally { + testFile.delete(); + } + } + + @Test + @RequiresFlagsEnabled(android.content.res.Flags.FLAG_MANIFEST_FLAGGING) + @EnableFlags(android.content.res.Flags.FLAG_USE_NEW_ACONFIG_STORAGE) + public void testParseWithFeatureFlagAttributes_newStorage() throws Exception { final File testFile = extractFile(TEST_APP8_APK); try (PackageParser2 parser = new TestPackageParser2()) { Map<String, Boolean> flagValues = new HashMap<>(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 7d3cd8a8a9ae..38de7ce013c2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -34,6 +34,7 @@ import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REM import static com.android.server.display.DisplayDeviceInfo.DIFF_EVERYTHING; import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED; +import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED; @@ -1180,13 +1181,21 @@ public class LogicalDisplayMapperTest { assertEquals(LOGICAL_DISPLAY_EVENT_STATE_CHANGED, mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); + // Change the display committed state + when(mFlagsMock.isCommittedStateSeparateEventEnabled()).thenReturn(true); + newDisplayInfo = new DisplayInfo(); + newDisplayInfo.committedState = STATE_OFF; + assertEquals(LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED, + mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); // Change multiple properties newDisplayInfo = new DisplayInfo(); newDisplayInfo.refreshRateOverride = 30; newDisplayInfo.state = STATE_OFF; + newDisplayInfo.committedState = STATE_OFF; assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED - | LOGICAL_DISPLAY_EVENT_STATE_CHANGED, + | LOGICAL_DISPLAY_EVENT_STATE_CHANGED + | LOGICAL_DISPLAY_EVENT_COMMITTED_STATE_CHANGED, mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index 2cd105ba5317..67b26c1c0b00 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -60,6 +60,8 @@ import android.content.ComponentName; import android.content.pm.PackageManagerInternal; import android.net.Uri; import android.os.SystemClock; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.MediaStore; import android.util.SparseIntArray; @@ -71,6 +73,7 @@ import com.android.server.job.JobSchedulerService; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -92,6 +95,9 @@ public class JobStatusTest { private static final Uri IMAGES_MEDIA_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; private static final Uri VIDEO_MEDIA_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private JobSchedulerInternal mJobSchedulerInternal; private MockitoSession mMockingSession; @@ -1373,6 +1379,86 @@ public class JobStatusTest { assertEquals("@TestNamespace@TestTag:foo", jobStatus.getBatteryName()); } + @Test + @EnableFlags({ + com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME, + android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS + }) + public void testJobName_NotTagNoNamespace_IncludeTraceTagInJobNameEnabled() { + JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("TestTraceTag") + .build(); + JobStatus jobStatus = createJobStatus(jobInfo, null, -1, null, null); + assertEquals("#TestTraceTag#foo/bar", jobStatus.getBatteryName()); + } + + @Test + @EnableFlags({ + com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME, + android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS + }) + public void testJobName_NoTagWithNamespace_IncludeTraceTagInJobNameEnabled() { + JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("TestTraceTag") + .build(); + JobStatus jobStatus = createJobStatus(jobInfo, null, -1, "TestNamespace", null); + assertEquals("#TestTraceTag#@TestNamespace@foo/bar", jobStatus.getBatteryName()); + } + + @Test + @EnableFlags({ + com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME, + android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS + }) + public void testJobName_WithTagNoNamespace_IncludeTraceTagInJobNameEnabled() { + JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("TestTraceTag") + .build(); + JobStatus jobStatus = createJobStatus(jobInfo, SOURCE_PACKAGE, 0, null, "TestTag"); + assertEquals("#TestTraceTag#TestTag:foo", jobStatus.getBatteryName()); + } + + @Test + @EnableFlags({ + com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME, + android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS + }) + public void testJobName_FilteredTraceTagEmail_IncludeTraceTagInJobNameEnabled() { + JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("test@email.com") + .build(); + JobStatus jobStatus = createJobStatus(jobInfo, SOURCE_PACKAGE, 0, null, "TestTag"); + assertEquals("#[EMAIL]#TestTag:foo", jobStatus.getBatteryName()); + } + + @Test + @EnableFlags({ + com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME, + android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS + }) + public void testJobName_FilteredTraceTagPhone_IncludeTraceTagInJobNameEnabled() { + JobInfo jobInfo = new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("123-456-7890") + .build(); + JobStatus jobStatus = createJobStatus(jobInfo, SOURCE_PACKAGE, 0, null, "TestTag"); + assertEquals("#[PHONE]#TestTag:foo", jobStatus.getBatteryName()); + } + + @Test + @EnableFlags({ + com.android.server.job.Flags.FLAG_INCLUDE_TRACE_TAG_IN_JOB_NAME, + android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS + }) + public void testJobName_WithTagAndNamespace_IncludeTraceTagInJobNameEnabled() { + JobInfo jobInfo = + new JobInfo.Builder(101, new ComponentName("foo", "bar")) + .setTraceTag("TestTraceTag") + .build(); + JobStatus jobStatus = + createJobStatus(jobInfo, SOURCE_PACKAGE, 0, "TestNamespace", "TestTag"); + assertEquals("#TestTraceTag#@TestNamespace@TestTag:foo", jobStatus.getBatteryName()); + } + private void markExpeditedQuotaApproved(JobStatus job, boolean isApproved) { if (job.isRequestedExpeditedJob()) { job.setExpeditedJobQuotaApproved(sElapsedRealtimeClock.millis(), isApproved); diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java index 83a390d7f70b..4e56422ec391 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java @@ -437,6 +437,42 @@ public class NotifierTest { } @Test + public void testOnGroupChanged_perDisplayWakeByTouchEnabled() { + createNotifier(); + // GIVEN per-display wake by touch is enabled and one display group has been defined with + // two displays + when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(true); + final int groupId = 121; + final int displayId1 = 1221; + final int displayId2 = 1222; + final int[] displays = new int[]{displayId1, displayId2}; + when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displays)); + when(mDisplayManagerInternal.getDisplayIdsForGroup(groupId)).thenReturn(displays); + SparseArray<int[]> displayIdsByGroupId = new SparseArray<>(); + displayIdsByGroupId.put(groupId, displays); + when(mDisplayManagerInternal.getDisplayIdsByGroupsIds()).thenReturn(displayIdsByGroupId); + mNotifier.onGroupWakefulnessChangeStarted( + groupId, WAKEFULNESS_AWAKE, PowerManager.WAKE_REASON_TAP, /* eventTime= */ 1000); + final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray(); + expectedDisplayInteractivities.put(displayId1, true); + expectedDisplayInteractivities.put(displayId2, true); + verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities); + + // WHEN display group is changed to only contain one display + SparseArray<int[]> newDisplayIdsByGroupId = new SparseArray<>(); + newDisplayIdsByGroupId.put(groupId, new int[]{displayId1}); + when(mDisplayManagerInternal.getDisplayIdsByGroupsIds()).thenReturn(newDisplayIdsByGroupId); + mNotifier.onGroupChanged(); + + // THEN native input manager is informed that the displays in the group have changed + final SparseBooleanArray expectedDisplayInteractivitiesAfterChange = + new SparseBooleanArray(); + expectedDisplayInteractivitiesAfterChange.put(displayId1, true); + verify(mInputManagerInternal).setDisplayInteractivities( + expectedDisplayInteractivitiesAfterChange); + } + + @Test public void testOnWakeLockReleased_FrameworkStatsLogged_NoChains() { when(mPowerManagerFlags.isMoveWscLoggingToNotifierEnabled()).thenReturn(true); createNotifier(); diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index 29a17e1c85ab..ff6796561926 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -2501,6 +2501,49 @@ public class PowerManagerServiceTest { } @Test + public void testMultiDisplay_twoDisplays_onlyDefaultDisplayCanDream() { + final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; + final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1; + final AtomicReference<DisplayManagerInternal.DisplayGroupListener> listener = + new AtomicReference<>(); + doAnswer((Answer<Void>) invocation -> { + listener.set(invocation.getArgument(0)); + return null; + }).when(mDisplayManagerInternalMock).registerDisplayGroupListener(any()); + final DisplayInfo info = new DisplayInfo(); + info.displayGroupId = nonDefaultDisplayGroupId; + when(mDisplayManagerInternalMock.getDisplayInfo(nonDefaultDisplay)).thenReturn(info); + when(mBatteryManagerInternalMock.isPowered(anyInt())).thenReturn(true); + Settings.Secure.putInt(mContextSpy.getContentResolver(), + Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1); + doAnswer(inv -> { + when(mDreamManagerInternalMock.isDreaming()).thenReturn(true); + return null; + }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString()); + + setMinimumScreenOffTimeoutConfig(5); + createService(); + startSystem(); + + listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId); + + assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( + WAKEFULNESS_AWAKE); + assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo( + WAKEFULNESS_AWAKE); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); + + advanceTime(15000); + + // Only the default display group is dreaming. + assertThat(mService.getWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( + WAKEFULNESS_DREAMING); + assertThat(mService.getWakefulnessLocked(nonDefaultDisplayGroupId)).isEqualTo( + WAKEFULNESS_DOZING); + assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); + } + + @Test public void testMultiDisplay_addNewDisplay_becomeGloballyAwakeButDefaultRemainsDozing() { final int nonDefaultDisplayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1; final int nonDefaultDisplay = Display.DEFAULT_DISPLAY + 1; diff --git a/services/tests/powerstatstests/res/raw/battery-history.zip b/services/tests/powerstatstests/res/raw/battery-history.zip Binary files differnew file mode 100644 index 000000000000..ed82ac0f79cc --- /dev/null +++ b/services/tests/powerstatstests/res/raw/battery-history.zip diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java new file mode 100644 index 000000000000..8fc8c9f677a6 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderPerfTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.power.stats; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.res.Resources; +import android.os.BatteryConsumer; +import android.os.BatteryUsageStats; +import android.os.BatteryUsageStatsQuery; +import android.os.ConditionVariable; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; + +import com.android.internal.os.Clock; +import com.android.internal.os.CpuScalingPolicies; +import com.android.internal.os.CpuScalingPolicyReader; +import com.android.internal.os.MonotonicClock; +import com.android.internal.os.PowerProfile; +import com.android.server.power.stats.processor.MultiStatePowerAttributor; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +@RunWith(AndroidJUnit4.class) +@LargeTest +@android.platform.test.annotations.DisabledOnRavenwood(reason = "Performance test") +@Ignore("Performance experiment. Comment out @Ignore to run") +public class BatteryUsageStatsProviderPerfTest { + @Rule + public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private final Clock mClock = new MockClock(); + private MonotonicClock mMonotonicClock; + private PowerProfile mPowerProfile; + private CpuScalingPolicies mCpuScalingPolicies; + private File mDirectory; + private Handler mHandler; + private MockBatteryStatsImpl mBatteryStats; + + @Before + public void setup() throws Exception { + Context context = InstrumentationRegistry.getContext(); + mPowerProfile = new PowerProfile(context); + mCpuScalingPolicies = new CpuScalingPolicyReader().read(); + + HandlerThread mHandlerThread = new HandlerThread("batterystats-handler"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + // Extract accumulated battery history to ensure consistent iterations + mDirectory = Files.createTempDirectory("BatteryUsageStatsProviderPerfTest").toFile(); + File historyDirectory = new File(mDirectory, "battery-history"); + historyDirectory.mkdir(); + + long maxMonotonicTime = 0; + + // To recreate battery-history.zip if necessary, perform these commands: + // cd /tmp + // mkdir battery-history + // adb pull /data/system/battery-history + // zip battery-history.zip battery-history/* + // cp battery-history.zip \ + // $ANDROID_BUILD_TOP/frameworks/base/services/tests/powerstatstests/res/raw + Resources resources = context.getResources(); + int resId = resources.getIdentifier("battery-history", "raw", context.getPackageName()); + try (InputStream in = resources.openRawResource(resId)) { + try (ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry ze; + while ((ze = zis.getNextEntry()) != null) { + if (!ze.getName().endsWith(".bh")) { + continue; + } + File file = new File(mDirectory, ze.getName()); + try (OutputStream out = new FileOutputStream( + file)) { + FileUtils.copy(zis, out); + } + long timestamp = Long.parseLong(file.getName().replace(".bh", "")); + if (timestamp > maxMonotonicTime) { + maxMonotonicTime = timestamp; + } + } + } + } + + mMonotonicClock = new MonotonicClock(maxMonotonicTime + 1000000000, mClock); + mBatteryStats = new MockBatteryStatsImpl(mClock, mDirectory); + } + + @Test + public void getBatteryUsageStats_accumulated() { + BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() + .setMaxStatsAgeMs(0) + .includePowerStateData() + .includeScreenStateData() + .includeProcessStateData() + .accumulated() + .build(); + + double expectedCpuPower = 0; + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + state.pauseTiming(); + + waitForBackgroundThread(); + + BatteryUsageStatsProvider provider = createBatteryUsageStatsProvider(); + state.resumeTiming(); + + BatteryUsageStats stats = provider.getBatteryUsageStats(mBatteryStats, query); + waitForBackgroundThread(); + + state.pauseTiming(); + + double cpuConsumedPower = stats.getAggregateBatteryConsumer( + BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE) + .getConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU); + assertThat(cpuConsumedPower).isNonZero(); + if (expectedCpuPower == 0) { + expectedCpuPower = cpuConsumedPower; + } else { + // Verify that all iterations produce the same result + assertThat(cpuConsumedPower).isEqualTo(expectedCpuPower); + } + state.resumeTiming(); + } + } + + private BatteryUsageStatsProvider createBatteryUsageStatsProvider() { + Context context = InstrumentationRegistry.getContext(); + + PowerStatsStore store = new PowerStatsStore(mDirectory, mHandler); + store.reset(); + + MultiStatePowerAttributor powerAttributor = new MultiStatePowerAttributor(context, store, + mPowerProfile, mCpuScalingPolicies, mPowerProfile::getBatteryCapacity); + return new BatteryUsageStatsProvider(context, powerAttributor, mPowerProfile, + mCpuScalingPolicies, store, 10000000, mClock, mMonotonicClock); + } + + private void waitForBackgroundThread() { + ConditionVariable done = new ConditionVariable(); + mHandler.post(done::open); + done.block(); + } +} diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java index 298d27e2e8c4..879aa4893802 100644 --- a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java +++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java @@ -17,7 +17,6 @@ package com.android.server.security.intrusiondetection; import static android.Manifest.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE; -import static android.Manifest.permission.INTERNET; import static android.Manifest.permission.MANAGE_INTRUSION_DETECTION_STATE; import static android.Manifest.permission.READ_INTRUSION_DETECTION_STATE; @@ -28,7 +27,6 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -37,8 +35,8 @@ import static org.mockito.Mockito.verify; import android.annotation.SuppressLint; import android.app.admin.ConnectEvent; +import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.DnsEvent; -import android.app.admin.SecurityLog; import android.app.admin.SecurityLog.SecurityEvent; import android.content.ComponentName; import android.content.Context; @@ -53,37 +51,22 @@ import android.os.test.TestLooper; import android.security.intrusiondetection.IIntrusionDetectionServiceCommandCallback; import android.security.intrusiondetection.IIntrusionDetectionServiceStateCallback; import android.security.intrusiondetection.IntrusionDetectionEvent; -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; import android.util.Log; import androidx.test.core.app.ApplicationProvider; import com.android.bedstead.harrier.BedsteadJUnit4; import com.android.bedstead.multiuser.annotations.RequireRunOnSystemUser; -import com.android.bedstead.nene.TestApis; -import com.android.bedstead.nene.devicepolicy.DeviceOwner; import com.android.bedstead.permissions.CommonPermissions; -import com.android.bedstead.permissions.PermissionContext; import com.android.bedstead.permissions.annotations.EnsureHasPermission; import com.android.coretests.apps.testapp.LocalIntrusionDetectionEventTransport; import com.android.server.ServiceThread; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.KeyStore; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -107,7 +90,6 @@ public class IntrusionDetectionServiceTest { private static final int ERROR_DATA_SOURCE_UNAVAILABLE = IIntrusionDetectionServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE; - private static DeviceOwner sDeviceOwner; private Context mContext; private IntrusionDetectionEventTransportConnection mIntrusionDetectionEventTransportConnection; @@ -124,6 +106,8 @@ public class IntrusionDetectionServiceTest { "com.android.coretests.apps.testapp"; private static final String TEST_SERVICE = TEST_PKG + ".TestLoggingService"; + DevicePolicyManagerInternal mDevicePolicyManagerInternal; + @SuppressLint("VisibleForTests") @Before public void setUp() throws Exception { @@ -189,6 +173,7 @@ public class IntrusionDetectionServiceTest { } @Test + @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void testAddStateCallback_Disabled_TwoStateCallbacks() throws RemoteException { StateCallback scb1 = new StateCallback(); assertEquals(STATE_UNKNOWN, scb1.mState); @@ -204,7 +189,7 @@ public class IntrusionDetectionServiceTest { } @Test - @Ignore("Unit test does not run as system service UID") + @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void testRemoveStateCallback() throws RemoteException { mIntrusionDetectionService.setState(STATE_DISABLED); StateCallback scb1 = new StateCallback(); @@ -220,15 +205,19 @@ public class IntrusionDetectionServiceTest { mIntrusionDetectionService.getBinderService().removeStateCallback(scb2); CommandCallback ccb = new CommandCallback(); + + // Enable will fail; caller does not run as system server. + doNothing().when(mDataAggregator).enable(); mIntrusionDetectionService.getBinderService().enable(ccb); + mTestLooper.dispatchAll(); assertEquals(STATE_ENABLED, scb1.mState); assertEquals(STATE_DISABLED, scb2.mState); assertNull(ccb.mErrorCode); } - @Ignore("Unit test does not run as system service UID") @Test + @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void testEnable_FromDisabled_TwoStateCallbacks() throws RemoteException { mIntrusionDetectionService.setState(STATE_DISABLED); StateCallback scb1 = new StateCallback(); @@ -243,6 +232,9 @@ public class IntrusionDetectionServiceTest { CommandCallback ccb = new CommandCallback(); mIntrusionDetectionService.getBinderService().enable(ccb); + + // Enable will fail; caller does not run as system server. + doNothing().when(mDataAggregator).enable(); mTestLooper.dispatchAll(); verify(mDataAggregator, times(1)).enable(); @@ -319,7 +311,7 @@ public class IntrusionDetectionServiceTest { assertNull(ccb.mErrorCode); } - @Ignore("Enable once the IntrusionDetectionEventTransportConnection is ready") + @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) @Test public void testEnable_FromDisable_TwoStateCallbacks_TransportUnavailable() throws RemoteException { @@ -390,146 +382,6 @@ public class IntrusionDetectionServiceTest { } @Test - @Ignore("Unit test does not run as system service UID") - @RequireRunOnSystemUser - @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) - public void testDataAggregator_AddSecurityEvent() throws Exception { - mIntrusionDetectionService.setState(STATE_ENABLED); - ServiceThread mockThread = spy(ServiceThread.class); - mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread); - - // SecurityLogging generates a number of events and callbacks, so create a latch to wait for - // the given event. - String eventString = this.getClass().getName() + ".testSecurityEvent"; - - final CountDownLatch latch = new CountDownLatch(1); - // TODO: Replace this mock when the IntrusionDetectionEventTransportConnection is ready. - doAnswer( - new Answer<Boolean>() { - @Override - public Boolean answer(InvocationOnMock input) { - List<IntrusionDetectionEvent> receivedEvents = - (List<IntrusionDetectionEvent>) input.getArguments()[0]; - for (IntrusionDetectionEvent event : receivedEvents) { - if (event.getType() == IntrusionDetectionEvent.SECURITY_EVENT) { - SecurityEvent securityEvent = event.getSecurityEvent(); - Object[] eventData = (Object[]) securityEvent.getData(); - if (securityEvent.getTag() == SecurityLog.TAG_KEY_GENERATED - && eventData[1].equals(eventString)) { - latch.countDown(); - } - } - } - return true; - } - }) - .when(mIntrusionDetectionEventTransportConnection).addData(any()); - mDataAggregator.enable(); - - // Generate the security event. - generateSecurityEvent(eventString); - TestApis.devicePolicy().forceSecurityLogs(); - - // Verify the event is received. - mTestLooper.startAutoDispatch(); - assertTrue(latch.await(1, TimeUnit.SECONDS)); - mTestLooper.stopAutoDispatch(); - - mDataAggregator.disable(); - } - - @Test - @RequireRunOnSystemUser - @Ignore("Unit test does not run as system service UID") - @EnsureHasPermission(CommonPermissions.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) - public void testDataAggregator_AddNetworkEvent() throws Exception { - mIntrusionDetectionService.setState(STATE_ENABLED); - ServiceThread mockThread = spy(ServiceThread.class); - mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread); - - // Network logging may log multiple and callbacks, so create a latch to wait for - // the given event. - // eventServer must be a valid domain to generate a network log event. - String eventServer = "google.com"; - final CountDownLatch latch = new CountDownLatch(1); - // TODO: Replace this mock when the IntrusionDetectionEventTransportConnection is ready. - doAnswer( - new Answer<Boolean>() { - @Override - public Boolean answer(InvocationOnMock input) { - List<IntrusionDetectionEvent> receivedEvents = - (List<IntrusionDetectionEvent>) input.getArguments()[0]; - for (IntrusionDetectionEvent event : receivedEvents) { - if (event.getType() - == IntrusionDetectionEvent.NETWORK_EVENT_DNS) { - DnsEvent dnsEvent = event.getDnsEvent(); - if (dnsEvent.getHostname().equals(eventServer)) { - latch.countDown(); - } - } - } - return true; - } - }) - .when(mIntrusionDetectionEventTransportConnection).addData(any()); - mDataAggregator.enable(); - - // Generate the network event. - generateNetworkEvent(eventServer); - TestApis.devicePolicy().forceNetworkLogs(); - - // Verify the event is received. - mTestLooper.startAutoDispatch(); - assertTrue(latch.await(1, TimeUnit.SECONDS)); - mTestLooper.stopAutoDispatch(); - - mDataAggregator.disable(); - } - - /** Emits a given string into security log (if enabled). */ - private void generateSecurityEvent(String eventString) - throws IllegalArgumentException, GeneralSecurityException, IOException { - if (eventString == null || eventString.isEmpty()) { - throw new IllegalArgumentException( - "Error generating security event: eventString must not be empty"); - } - - final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"); - keyGen.initialize( - new KeyGenParameterSpec.Builder(eventString, KeyProperties.PURPOSE_SIGN).build()); - // Emit key generation event. - final KeyPair keyPair = keyGen.generateKeyPair(); - assertNotNull(keyPair); - - final KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); - ks.load(null); - // Emit key destruction event. - ks.deleteEntry(eventString); - } - - /** Emits a given string into network log (if enabled). */ - private void generateNetworkEvent(String server) throws IllegalArgumentException, IOException { - if (server == null || server.isEmpty()) { - throw new IllegalArgumentException( - "Error generating network event: server must not be empty"); - } - - HttpURLConnection urlConnection = null; - int connectionTimeoutMS = 2_000; - try (PermissionContext p = TestApis.permissions().withPermission(INTERNET)) { - final URL url = new URL("http://" + server); - urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setConnectTimeout(connectionTimeoutMS); - urlConnection.setReadTimeout(connectionTimeoutMS); - urlConnection.getResponseCode(); - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - } - - @Test @RequireRunOnSystemUser @EnsureHasPermission( android.Manifest.permission.BIND_INTRUSION_DETECTION_EVENT_TRANSPORT_SERVICE) diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java index b5a538fa09f8..c7da27420cbb 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java @@ -103,7 +103,7 @@ public class AudioDeviceInventoryTest { // NOTE: for now this is only when flag asDeviceConnectionFailure is true if (asDeviceConnectionFailure()) { when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT)) + AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/)) .thenReturn(AudioSystem.AUDIO_STATUS_ERROR); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, @@ -115,7 +115,7 @@ public class AudioDeviceInventoryTest { // test that the device is added when AudioSystem returns AUDIO_STATUS_OK // when setDeviceConnectionState is called for the connection when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, - AudioSystem.AUDIO_FORMAT_DEFAULT)) + AudioSystem.AUDIO_FORMAT_DEFAULT, false /*deviceSwitch*/)) .thenReturn(AudioSystem.AUDIO_STATUS_OK); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java index ce59a86c6ca3..39e7d727f7c5 100644 --- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java +++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java @@ -51,9 +51,9 @@ public class NoOpAudioSystemAdapter extends AudioSystemAdapter { // Overrides of AudioSystemAdapter @Override public int setDeviceConnectionState(AudioDeviceAttributes attributes, int state, - int codecFormat) { - Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s", - attributes.toString(), state, Integer.toHexString(codecFormat))); + int codecFormat, boolean deviceSwitch) { + Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %d, 0x%s %b", + attributes.toString(), state, Integer.toHexString(codecFormat), deviceSwitch)); return AudioSystem.AUDIO_STATUS_OK; } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 30aa8cebdff6..e0023e59af50 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -1768,6 +1768,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore // b/396073342 public void testCertificateDisclosure() throws Exception { final int userId = CALLER_USER_HANDLE; final UserHandle user = UserHandle.of(userId); @@ -4612,6 +4613,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore // b/396073342 public void testGetLastBugReportRequestTime() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -4659,6 +4661,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore // b/396073342 public void testGetLastNetworkLogRetrievalTime() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); @@ -6441,6 +6444,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore // b/396073342 public void testGetOwnerInstalledCaCertsForDeviceOwner() throws Exception { mServiceContext.packageName = mRealTestContext.getPackageName(); mServiceContext.applicationInfo = new ApplicationInfo(); @@ -6452,6 +6456,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore // b/396073342 public void testGetOwnerInstalledCaCertsForProfileOwner() throws Exception { mServiceContext.packageName = mRealTestContext.getPackageName(); mServiceContext.applicationInfo = new ApplicationInfo(); @@ -6464,6 +6469,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore // b/396073342 public void testGetOwnerInstalledCaCertsForDelegate() throws Exception { mServiceContext.packageName = mRealTestContext.getPackageName(); mServiceContext.applicationInfo = new ApplicationInfo(); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java index fca0cfbc7d2f..cf2c15c5daca 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java @@ -432,7 +432,7 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java index ec44a918f8e8..f44517a47f55 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java @@ -112,7 +112,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @@ -135,7 +135,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @Test @@ -160,7 +160,7 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav .setMaxVolumeIndex(AudioStatus.MAX_VOLUME) .setMinVolumeIndex(AudioStatus.MIN_VOLUME) .build()), - any(), any(), anyBoolean()); + anyBoolean(), any(), any()); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java index 7294ba62cdae..90f94cb4b596 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioFramework.java @@ -183,9 +183,9 @@ public class FakeAudioFramework { public void setDeviceAbsoluteVolumeBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE); } @@ -193,9 +193,9 @@ public class FakeAudioFramework { public void setDeviceAbsoluteVolumeAdjustOnlyBehavior( @NonNull AudioDeviceAttributes device, @NonNull VolumeInfo volume, + boolean handlesVolumeAdjustment, @NonNull @CallbackExecutor Executor executor, - @NonNull OnAudioDeviceVolumeChangedListener vclistener, - boolean handlesVolumeAdjustment) { + @NonNull OnAudioDeviceVolumeChangedListener vclistener) { setVolumeBehaviorHelper(device, AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_ADJUST_ONLY); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index f74e2ace7ae3..563baacf5811 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -66,6 +66,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -1033,6 +1034,7 @@ public class HdmiCecLocalDeviceTvTest { } @Test + @Ignore("b/360768278") public void onHotplug_doNotSend_systemAudioModeRequestWithParameter(){ // Add a device to the network and assert that this device is included in the list of // devices. diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 0eb20eb22380..66d7611a29c6 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -32,6 +32,7 @@ android_test { "androidx.test.rules", "hamcrest-library", "mockito-target-inline-minus-junit4", + "mockito-target-extended", "platform-compat-test-rules", "platform-test-annotations", "platformprotosnano", diff --git a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java index 779fa1aa2f72..dbbe40fd42e6 100644 --- a/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java +++ b/services/tests/uiservicestests/src/android/app/NotificationManagerZenTest.java @@ -80,7 +80,7 @@ public class NotificationManagerZenTest { } @Test - @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @RequiresFlagsEnabled(Flags.FLAG_MODES_UI) public void setAutomaticZenRuleState_manualActivation() { AutomaticZenRule ruleToCreate = createZenRule("rule"); String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); @@ -111,7 +111,7 @@ public class NotificationManagerZenTest { } @Test - @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @RequiresFlagsEnabled(Flags.FLAG_MODES_UI) public void setAutomaticZenRuleState_manualDeactivation() { AutomaticZenRule ruleToCreate = createZenRule("rule"); String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); @@ -145,7 +145,7 @@ public class NotificationManagerZenTest { } @Test - @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @RequiresFlagsEnabled(Flags.FLAG_MODES_UI) public void setAutomaticZenRuleState_respectsManuallyActivated() { AutomaticZenRule ruleToCreate = createZenRule("rule"); String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); @@ -178,7 +178,7 @@ public class NotificationManagerZenTest { } @Test - @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @RequiresFlagsEnabled(Flags.FLAG_MODES_UI) public void setAutomaticZenRuleState_respectsManuallyDeactivated() { AutomaticZenRule ruleToCreate = createZenRule("rule"); String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); @@ -212,7 +212,7 @@ public class NotificationManagerZenTest { } @Test - @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @RequiresFlagsEnabled(Flags.FLAG_MODES_UI) public void setAutomaticZenRuleState_manualActivationFromApp() { AutomaticZenRule ruleToCreate = createZenRule("rule"); String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); @@ -244,7 +244,7 @@ public class NotificationManagerZenTest { } @Test - @RequiresFlagsEnabled({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI}) + @RequiresFlagsEnabled(Flags.FLAG_MODES_UI) public void setAutomaticZenRuleState_manualDeactivationFromApp() { AutomaticZenRule ruleToCreate = createZenRule("rule"); String ruleId = mNotificationManager.addAutomaticZenRule(ruleToCreate); diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index c4b8599a483c..9930c9f07ed8 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -70,7 +70,6 @@ import android.Manifest; import android.app.Activity; import android.app.ActivityManager; import android.app.AlarmManager; -import android.app.Flags; import android.app.IOnProjectionStateChangedListener; import android.app.IUiModeManager; import android.content.BroadcastReceiver; @@ -91,7 +90,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.test.FakePermissionEnforcer; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; @@ -1508,13 +1506,11 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void testAttentionModeThemeOverlay_nightModeDisabled() throws RemoteException { testAttentionModeThemeOverlay(false); } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void testAttentionModeThemeOverlay_nightModeEnabled() throws RemoteException { testAttentionModeThemeOverlay(true); } diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java index b3ec2153542a..c9d5241c57b7 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java +++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java @@ -30,6 +30,7 @@ import android.testing.TestableContext; import androidx.test.InstrumentationRegistry; +import com.android.server.pm.UserManagerInternal; import com.android.server.uri.UriGrantsManagerInternal; import org.junit.After; @@ -41,6 +42,7 @@ import org.mockito.MockitoAnnotations; public class UiServiceTestCase { @Mock protected PackageManagerInternal mPmi; + @Mock protected UserManagerInternal mUmi; @Mock protected UriGrantsManagerInternal mUgmInternal; protected static final String PKG_N_MR1 = "com.example.n_mr1"; @@ -92,6 +94,8 @@ public class UiServiceTestCase { } }); + LocalServices.removeServiceForTest(UserManagerInternal.class); + LocalServices.addService(UserManagerInternal.class, mUmi); LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal); when(mUgmInternal.checkGrantUriPermission( diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java index 1890879da69d..5ce9a3e8d4d4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java @@ -47,7 +47,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.hardware.display.ColorDisplayManager; import android.os.PowerManager; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenDeviceEffects; import android.testing.TestableContext; @@ -102,8 +101,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_appliesEffects() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) .setShouldDimWallpaper(true) @@ -119,7 +116,6 @@ public class DefaultDeviceEffectsApplierTest { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void apply_logsToZenLog() { when(mPowerManager.isInteractive()).thenReturn(true); ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = @@ -155,8 +151,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_removesEffects() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) .setShouldDimWallpaper(true) @@ -180,8 +174,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_removesOnlyPreviouslyAppliedEffects() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) .build(); @@ -197,7 +189,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_missingSomeServices_okay() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mContext.addMockSystemService(ColorDisplayManager.class, null); mContext.addMockSystemService(WallpaperManager.class, null); mApplier = new DefaultDeviceEffectsApplier(mContext); @@ -216,7 +207,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_disabledWallpaperService_dimWallpaperNotApplied() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); WallpaperManager disabledWallpaperService = mock(WallpaperManager.class); when(mWallpaperManager.isWallpaperSupported()).thenReturn(false); mContext.addMockSystemService(WallpaperManager.class, disabledWallpaperService); @@ -236,8 +226,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_someEffects_onlyThoseEffectsApplied() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .setShouldDisplayGrayscale(true) @@ -253,8 +241,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_onlyEffectDeltaApplied() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - mApplier.apply(new ZenDeviceEffects.Builder().setShouldDimWallpaper(true).build(), ORIGIN_USER_IN_SYSTEMUI); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); @@ -272,7 +258,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_nightModeFromApp_appliedOnScreenOff() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); ArgumentCaptor<IntentFilter> intentFilterCaptor = @@ -301,8 +286,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_nightModeWithScreenOff_appliedImmediately( @TestParameter ZenChangeOrigin origin) { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - when(mPowerManager.isInteractive()).thenReturn(false); mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(), @@ -314,7 +297,6 @@ public class DefaultDeviceEffectsApplierTest { } @Test - @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI}) public void apply_nightModeWithScreenOnAndKeyguardShowing_appliedImmediately( @TestParameter ZenChangeOrigin origin) { @@ -334,8 +316,6 @@ public class DefaultDeviceEffectsApplierTest { "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"}) public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin( ZenChangeOrigin origin) { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - when(mPowerManager.isInteractive()).thenReturn(true); mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(), @@ -351,8 +331,6 @@ public class DefaultDeviceEffectsApplierTest { "{origin: ORIGIN_SYSTEM}", "{origin: ORIGIN_UNKNOWN}"}) public void apply_nightModeWithScreenOn_willBeAppliedLaterBasedOnOrigin( ZenChangeOrigin origin) { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - when(mPowerManager.isInteractive()).thenReturn(true); mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(), @@ -367,8 +345,6 @@ public class DefaultDeviceEffectsApplierTest { @Test public void apply_servicesThrow_noCrash() { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); - doThrow(new RuntimeException()).when(mPowerManager) .suppressAmbientDisplay(anyString(), anyBoolean()); doThrow(new RuntimeException()).when(mColorDisplayManager).setSaturationLevel(anyInt()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index e5c42082ab97..98440ecdad82 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -17,12 +17,17 @@ package com.android.server.notification; import static android.content.Context.DEVICE_POLICY_SERVICE; import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; +import static android.os.UserHandle.USER_ALL; +import static android.os.UserHandle.USER_CURRENT; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER; +import static com.android.server.notification.Flags.managedServicesConcurrentMultiuser; import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT; import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE; import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled; @@ -66,7 +71,9 @@ import android.os.IInterface; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; @@ -83,6 +90,7 @@ import com.android.server.UiServiceTestCase; import com.google.android.collect.Lists; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -105,6 +113,9 @@ import java.util.concurrent.CountDownLatch; public class ManagedServicesTest extends UiServiceTestCase { + @Rule + public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private IPackageManager mIpm; @Mock @@ -155,6 +166,7 @@ public class ManagedServicesTest extends UiServiceTestCase { users.add(new UserInfo(11, "11", 0)); users.add(new UserInfo(12, "12", 0)); users.add(new UserInfo(13, "13", 0)); + users.add(new UserInfo(99, "99", 0)); for (UserInfo user : users) { when(mUm.getUserInfo(eq(user.id))).thenReturn(user); } @@ -804,6 +816,7 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void rebindServices_onlyBindsExactMatchesIfComponent() throws Exception { // If the primary and secondary lists contain component names, only those components within // the package should be matched @@ -841,6 +854,45 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void rebindServices_onlyBindsExactMatchesIfComponent_concurrent_multiUser() + throws Exception { + // If the primary and secondary lists contain component names, only those components within + // the package should be matched + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, + mIpm, + ManagedServices.APPROVAL_BY_COMPONENT); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + packages.add("anotherPackage"); + addExpectedServices(service, packages, 0); + + // only 2 components are approved per package + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryComponentNames.put(0, "anotherPackage/C1:anotherPackage/C2"); + + loadXml(service); + // verify the 2 components per package are enabled (bound) + verifyExpectedBoundEntries(service, true, 0); + verifyExpectedBoundEntries(service, false, 0); + + // verify the last component per package is not enabled/we don't try to bind to it + for (String pkg : packages) { + ComponentName unapprovedAdditionalComponent = + ComponentName.unflattenFromString(pkg + "/C3"); + assertFalse( + service.isComponentEnabledForUser( + unapprovedAdditionalComponent, 0)); + verify(mIpm, never()).getServiceInfo( + eq(unapprovedAdditionalComponent), anyLong(), anyInt()); + } + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void rebindServices_bindsEverythingInAPackage() throws Exception { // If the primary and secondary lists contain packages, all components within those packages // should be bound @@ -866,6 +918,32 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void rebindServices_bindsEverythingInAPackage_concurrent_multiUser() throws Exception { + // If the primary and secondary lists contain packages, all components within those packages + // should be bound + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_PACKAGE); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + packages.add("packagea"); + addExpectedServices(service, packages, 0); + + // 2 approved packages + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryPackages.put(0, "package"); + mExpectedSecondaryPackages.clear(); + mExpectedSecondaryPackages.put(0, "packagea"); + + loadXml(service); + + // verify the 3 components per package are enabled (bound) + verifyExpectedBoundEntries(service, true, 0); + verifyExpectedBoundEntries(service, false, 0); + } + + @Test public void reregisterService_checksAppIsApproved_pkg() throws Exception { Context context = mock(Context.class); PackageManager pm = mock(PackageManager.class); @@ -1118,6 +1196,7 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void testUpgradeAppBindsNewServices() throws Exception { // If the primary and secondary lists contain component names, only those components within // the package should be matched @@ -1159,6 +1238,49 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUpgradeAppBindsNewServices_concurrent_multiUser() throws Exception { + // If the primary and secondary lists contain component names, only those components within + // the package should be matched + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, + mIpm, + ManagedServices.APPROVAL_BY_PACKAGE); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + // only 2 components are approved per package + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + // new component expected + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2:package/C3"); + + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + // verify the 3 components per package are enabled (bound) + verifyExpectedBoundEntries(service, true, 0); + + // verify the last component per package is not enabled/we don't try to bind to it + for (String pkg : packages) { + ComponentName unapprovedAdditionalComponent = + ComponentName.unflattenFromString(pkg + "/C3"); + assertFalse( + service.isComponentEnabledForUser( + unapprovedAdditionalComponent, 0)); + verify(mIpm, never()).getServiceInfo( + eq(unapprovedAdditionalComponent), anyLong(), anyInt()); + } + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void testUpgradeAppNoPermissionNoRebind() throws Exception { Context context = spy(getContext()); doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); @@ -1211,6 +1333,59 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUpgradeAppNoPermissionNoRebind_concurrent_multiUser() throws Exception { + Context context = spy(getContext()); + doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any()); + + ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, + mIpm, + APPROVAL_BY_COMPONENT); + + List<String> packages = new ArrayList<>(); + packages.add("package"); + addExpectedServices(service, packages, 0); + + final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1"); + final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2"); + + // Both components are approved initially + mExpectedPrimaryComponentNames.clear(); + mExpectedPrimaryPackages.clear(); + mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2"); + mExpectedSecondaryComponentNames.clear(); + mExpectedSecondaryPackages.clear(); + + loadXml(service); + + //Component package/C1 loses bind permission + when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer( + (Answer<ServiceInfo>) invocation -> { + ComponentName invocationCn = invocation.getArgument(0); + if (invocationCn != null) { + ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = invocationCn.getPackageName(); + serviceInfo.name = invocationCn.getClassName(); + if (invocationCn.equals(unapprovedComponent)) { + serviceInfo.permission = "none"; + } else { + serviceInfo.permission = service.getConfig().bindPermission; + } + serviceInfo.metaData = null; + return serviceInfo; + } + return null; + } + ); + + // Trigger package update + service.onPackagesChanged(false, new String[]{"package"}, new int[]{0}); + + assertFalse(service.isComponentEnabledForUser(unapprovedComponent, 0)); + assertTrue(service.isComponentEnabledForUser(approvedComponent, 0)); + } + + @Test public void testSetPackageOrComponentEnabled() throws Exception { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, @@ -1517,6 +1692,201 @@ public class ManagedServicesTest extends UiServiceTestCase { assertTrue(componentsToBind.get(10).contains(ComponentName.unflattenFromString("c/c"))); } + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testPopulateComponentsToBindWithNonProfileUser() { + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + + SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>(); + ArraySet<ComponentName> allowed0 = new ArraySet<>(); + allowed0.add(ComponentName.unflattenFromString("a/a")); + approvedComponentsByUser.put(0, allowed0); + ArraySet<ComponentName> allowed10 = new ArraySet<>(); + allowed10.add(ComponentName.unflattenFromString("b/b")); + approvedComponentsByUser.put(10, allowed10); + + int nonProfileUser = 99; + ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>(); + allowedForNonProfileUser.add(ComponentName.unflattenFromString("c/c")); + approvedComponentsByUser.put(nonProfileUser, allowedForNonProfileUser); + + IntArray users = new IntArray(); + users.add(nonProfileUser); + users.add(10); + users.add(0); + + SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(nonProfileUser)).thenReturn(true); + + service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser); + + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("a/a"), 0)); + assertTrue(service.isComponentEnabledForPackage("a", 0)); + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("b/b"), 10)); + assertTrue(service.isComponentEnabledForPackage("b", 0)); + assertTrue(service.isComponentEnabledForPackage("b", 10)); + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("c/c"), nonProfileUser)); + assertTrue(service.isComponentEnabledForPackage("c", nonProfileUser)); + } + + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testRebindService_profileUser() throws Exception { + final int profileUserId = 10; + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); + spyOn(mService); + ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass( + IntArray.class); + when(mService.allowRebindForParentUser()).thenReturn(true); + + mService.rebindServices(false, profileUserId); + + verify(mService).populateComponentsToBind(any(), captor.capture(), any()); + assertTrue(captor.getValue().contains(0)); + assertTrue(captor.getValue().contains(profileUserId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testRebindService_nonProfileUser() throws Exception { + final int userId = 99; + when(mUserProfiles.isProfileUser(userId, mContext)).thenReturn(false); + spyOn(mService); + ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass( + IntArray.class); + when(mService.allowRebindForParentUser()).thenReturn(true); + + mService.rebindServices(false, userId); + + verify(mService).populateComponentsToBind(any(), captor.capture(), any()); + assertFalse(captor.getValue().contains(0)); + assertTrue(captor.getValue().contains(userId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testRebindService_userAll() throws Exception { + final int userId = 99; + spyOn(mService); + spyOn(mService.mUmInternal); + when(mService.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true); + ArgumentCaptor<IntArray> captor = ArgumentCaptor.forClass( + IntArray.class); + when(mService.allowRebindForParentUser()).thenReturn(true); + + mService.rebindServices(false, USER_ALL); + + verify(mService).populateComponentsToBind(any(), captor.capture(), any()); + assertTrue(captor.getValue().contains(0)); + assertTrue(captor.getValue().contains(userId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testOnUserStoppedWithVisibleBackgroundUser() throws Exception { + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + int userId = 99; + SparseArray<ArraySet<ComponentName>> approvedComponentsByUser = new SparseArray<>(); + ArraySet<ComponentName> allowedForNonProfileUser = new ArraySet<>(); + allowedForNonProfileUser.add(ComponentName.unflattenFromString("a/a")); + approvedComponentsByUser.put(userId, allowedForNonProfileUser); + IntArray users = new IntArray(); + users.add(userId); + SparseArray<Set<ComponentName>> componentsToBind = new SparseArray<>(); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true); + service.populateComponentsToBind(componentsToBind, users, approvedComponentsByUser); + assertTrue(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("a/a"), userId)); + assertTrue(service.isComponentEnabledForPackage("a", userId)); + + service.onUserStopped(userId); + + assertFalse(service.isComponentEnabledForUser( + ComponentName.unflattenFromString("a/a"), userId)); + assertFalse(service.isComponentEnabledForPackage("a", userId)); + verify(service).unbindUserServices(eq(userId)); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUnbindServicesImpl_serviceOfForegroundUser() throws Exception { + int switchingUserId = 10; + int userId = 99; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(false); + + IInterface iInterface = mock(IInterface.class); + when(iInterface.asBinder()).thenReturn(mock(IBinder.class)); + + ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo( + iInterface, ComponentName.unflattenFromString("a/a"), userId, false, + mock(ServiceConnection.class), 26, 34); + + Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>(); + removableBoundServices.add(serviceInfo); + + when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices); + ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass( + SparseArray.class); + + service.unbindServicesImpl(switchingUserId, true); + + verify(service).unbindFromServices(captor.capture()); + + assertEquals(captor.getValue().size(), 1); + assertTrue(captor.getValue().indexOfKey(userId) != -1); + assertTrue(captor.getValue().get(userId).contains( + ComponentName.unflattenFromString("a/a"))); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUnbindServicesImpl_serviceOfVisibleBackgroundUser() throws Exception { + int switchingUserId = 10; + int userId = 99; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + spyOn(service); + spyOn(service.mUmInternal); + when(service.mUmInternal.isVisibleBackgroundFullUser(userId)).thenReturn(true); + + IInterface iInterface = mock(IInterface.class); + when(iInterface.asBinder()).thenReturn(mock(IBinder.class)); + + ManagedServices.ManagedServiceInfo serviceInfo = service.new ManagedServiceInfo( + iInterface, ComponentName.unflattenFromString("a/a"), userId, + false, mock(ServiceConnection.class), 26, 34); + + Set<ManagedServices.ManagedServiceInfo> removableBoundServices = new ArraySet<>(); + removableBoundServices.add(serviceInfo); + + when(service.getRemovableConnectedServices()).thenReturn(removableBoundServices); + ArgumentCaptor<SparseArray<Set<ComponentName>>> captor = ArgumentCaptor.forClass( + SparseArray.class); + + service.unbindServicesImpl(switchingUserId, true); + + verify(service).unbindFromServices(captor.capture()); + + assertEquals(captor.getValue().size(), 0); + } + @Test public void testOnNullBinding() throws Exception { Context context = mock(Context.class); @@ -1681,6 +2051,7 @@ public class ManagedServicesTest extends UiServiceTestCase { assertFalse(service.isBound(cn, mZero.id)); assertFalse(service.isBound(cn, mTen.id)); } + @Test public void testOnPackagesChanged_nullValuesPassed_noNullPointers() { for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) { @@ -2012,6 +2383,7 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isComponentEnabledForCurrentProfiles_isThreadSafe() throws InterruptedException { for (UserInfo userInfo : mUm.getUsers()) { mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true); @@ -2024,6 +2396,20 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isComponentEnabledForUser_isThreadSafe() throws InterruptedException { + for (UserInfo userInfo : mUm.getUsers()) { + mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true); + } + testThreadSafety(() -> { + mService.rebindServices(false, 0); + assertThat(mService.isComponentEnabledForUser( + new ComponentName("pkg1", "cmp1"), 0)).isTrue(); + }, 20, 30); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isComponentEnabledForCurrentProfiles_profileUserId() { final int profileUserId = 10; when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); @@ -2037,6 +2423,24 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isComponentEnabledForUser_profileUserId() { + final int profileUserId = 10; + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); + spyOn(mService); + doReturn(USER_CURRENT).when(mService).resolveUserId(anyInt()); + + // Only approve for parent user (0) + mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true); + + // Test that the component is enabled after calling rebindServices with profile userId (10) + mService.rebindServices(false, profileUserId); + assertThat(mService.isComponentEnabledForUser( + new ComponentName("pkg1", "cmp1"), profileUserId)).isTrue(); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isComponentEnabledForCurrentProfiles_profileUserId_NAS() { final int profileUserId = 10; when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); @@ -2054,6 +2458,25 @@ public class ManagedServicesTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isComponentEnabledForUser_profileUserId_NAS() { + final int profileUserId = 10; + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); + // Do not rebind for parent users (NAS use-case) + ManagedServices service = spy(mService); + when(service.allowRebindForParentUser()).thenReturn(false); + doReturn(USER_CURRENT).when(service).resolveUserId(anyInt()); + + // Only approve for parent user (0) + service.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true); + + // Test that the component is disabled after calling rebindServices with profile userId (10) + service.rebindServices(false, profileUserId); + assertThat(service.isComponentEnabledForUser( + new ComponentName("pkg1", "cmp1"), profileUserId)).isFalse(); + } + + @Test @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR) public void testManagedServiceInfoIsSystemUi() { ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, @@ -2069,6 +2492,48 @@ public class ManagedServicesTest extends UiServiceTestCase { assertThat(service0.isSystemUi()).isFalse(); } + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUserMatchesAndEnabled_profileUser() throws Exception { + int currentUserId = 10; + int profileUserId = 11; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo( + mock(IInterface.class), ComponentName.unflattenFromString("a/a"), currentUserId, + false, mock(ServiceConnection.class), 26, 34)); + + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId); + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId); + doReturn(true).when(listener).isEnabledForUser(); + doReturn(true).when(mUserProfiles).isCurrentProfile(anyInt()); + + assertThat(listener.enabledAndUserMatches(profileUserId)).isTrue(); + } + + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void testUserMatchesAndDisabled_visibleBackgroudUser() throws Exception { + int currentUserId = 10; + int profileUserId = 11; + int visibleBackgroundUserId = 12; + + ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm, + APPROVAL_BY_COMPONENT); + ManagedServices.ManagedServiceInfo listener = spy(service.new ManagedServiceInfo( + mock(IInterface.class), ComponentName.unflattenFromString("a/a"), profileUserId, + false, mock(ServiceConnection.class), 26, 34)); + + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(profileUserId); + doReturn(currentUserId).when(service.mUmInternal).getProfileParentId(currentUserId); + doReturn(visibleBackgroundUserId).when(service.mUmInternal) + .getProfileParentId(visibleBackgroundUserId); + doReturn(true).when(listener).isEnabledForUser(); + + assertThat(listener.enabledAndUserMatches(visibleBackgroundUserId)).isFalse(); + } + private void mockServiceInfoWithMetaData(List<ComponentName> componentNames, ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException { @@ -2247,26 +2712,47 @@ public class ManagedServicesTest extends UiServiceTestCase { private void verifyExpectedBoundEntries(ManagedServices service, boolean primary) throws Exception { + verifyExpectedBoundEntries(service, primary, UserHandle.USER_CURRENT); + } + + private void verifyExpectedBoundEntries(ManagedServices service, boolean primary, + int targetUserId) throws Exception { ArrayMap<Integer, String> verifyMap = primary ? mExpectedPrimary.get(service.mApprovalLevel) : mExpectedSecondary.get(service.mApprovalLevel); for (int userId : verifyMap.keySet()) { for (String packageOrComponent : verifyMap.get(userId).split(":")) { if (!TextUtils.isEmpty(packageOrComponent)) { if (service.mApprovalLevel == APPROVAL_BY_PACKAGE) { - assertTrue(packageOrComponent, - service.isComponentEnabledForPackage(packageOrComponent)); + if (managedServicesConcurrentMultiuser()) { + assertTrue(packageOrComponent, + service.isComponentEnabledForPackage(packageOrComponent, + targetUserId)); + } else { + assertTrue(packageOrComponent, + service.isComponentEnabledForPackage(packageOrComponent)); + } for (int i = 1; i <= 3; i++) { ComponentName componentName = ComponentName.unflattenFromString( packageOrComponent +"/C" + i); - assertTrue(service.isComponentEnabledForCurrentProfiles( - componentName)); + if (managedServicesConcurrentMultiuser()) { + assertTrue(service.isComponentEnabledForUser( + componentName, targetUserId)); + } else { + assertTrue(service.isComponentEnabledForCurrentProfiles( + componentName)); + } verify(mIpm, times(1)).getServiceInfo( eq(componentName), anyLong(), anyInt()); } } else { ComponentName componentName = ComponentName.unflattenFromString(packageOrComponent); - assertTrue(service.isComponentEnabledForCurrentProfiles(componentName)); + if (managedServicesConcurrentMultiuser()) { + assertTrue(service.isComponentEnabledForUser(componentName, + targetUserId)); + } else { + assertTrue(service.isComponentEnabledForCurrentProfiles(componentName)); + } verify(mIpm, times(1)).getServiceInfo( eq(componentName), anyLong(), anyInt()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 415e3accfa39..37ab541f12da 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -140,6 +140,7 @@ import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; +import static com.android.server.notification.Flags.FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER; import static com.android.server.notification.Flags.FLAG_REJECT_OLD_NOTIFICATIONS; import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY; import static com.android.server.notification.NotificationManagerService.BITMAP_DURATION; @@ -867,7 +868,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { && filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) { mPackageIntentReceiver = broadcastReceivers.get(i); } - if (filter.hasAction(Intent.ACTION_USER_SWITCHED) + if (filter.hasAction(Intent.ACTION_USER_STOPPED) + || filter.hasAction(Intent.ACTION_USER_SWITCHED) || filter.hasAction(Intent.ACTION_PROFILE_UNAVAILABLE) || filter.hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { // There may be multiple receivers, get the NMS one @@ -11028,7 +11030,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void testAddAutomaticZenRule_typeManagedCanBeUsedByDeviceOwners() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -11046,20 +11047,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void testAddAutomaticZenRule_typeManagedCanBeUsedBySystem() throws Exception { addAutomaticZenRule_restrictedRuleTypeCanBeUsedBySystem(AutomaticZenRule.TYPE_MANAGED); } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void testAddAutomaticZenRule_typeManagedCannotBeUsedByRegularApps() throws Exception { addAutomaticZenRule_restrictedRuleTypeCannotBeUsedByRegularApps( AutomaticZenRule.TYPE_MANAGED); } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void testAddAutomaticZenRule_typeBedtimeCanBeUsedByWellbeing() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -11082,7 +11080,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void testAddAutomaticZenRule_typeBedtimeCanBeUsedBySystem() throws Exception { reset(mPackageManagerInternal); when(mPackageManagerInternal.isSameApp(eq(mPkg), eq(mUid), anyInt())).thenReturn(true); @@ -11090,7 +11087,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void testAddAutomaticZenRule_typeBedtimeCannotBeUsedByRegularApps() throws Exception { reset(mPackageManagerInternal); when(mPackageManagerInternal.isSameApp(eq(mPkg), eq(mUid), anyInt())).thenReturn(true); @@ -11133,7 +11129,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void addAutomaticZenRule_fromUser_mappedToOriginUser() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.isSystemUid = true; @@ -11145,7 +11140,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void addAutomaticZenRule_fromSystemNotUser_mappedToOriginSystem() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.isSystemUid = true; @@ -11157,7 +11151,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void addAutomaticZenRule_fromApp_mappedToOriginApp() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -11169,7 +11162,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void addAutomaticZenRule_fromAppFromUser_blocked() throws Exception { setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -11179,7 +11171,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void updateAutomaticZenRule_fromUserFromSystem_allowed() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.isSystemUid = true; @@ -11191,7 +11182,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void updateAutomaticZenRule_fromUserFromApp_blocked() throws Exception { setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -11201,7 +11191,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void removeAutomaticZenRule_fromUserFromSystem_allowed() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); mService.isSystemUid = true; @@ -11213,7 +11202,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void removeAutomaticZenRule_fromUserFromApp_blocked() throws Exception { setUpMockZenTest(); mService.setCallerIsNormalPackage(); @@ -11223,7 +11211,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void setAutomaticZenRuleState_fromAppWithConditionFromUser_originUserInApp() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); @@ -11238,7 +11225,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void setAutomaticZenRuleState_fromAppWithConditionNotFromUser_originApp() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); @@ -11253,7 +11239,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void setAutomaticZenRuleState_fromSystemWithConditionFromUser_originUserInSystemUi() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); @@ -11267,7 +11252,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyInt()); } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void setAutomaticZenRuleState_fromSystemWithConditionNotFromUser_originSystem() throws Exception { ZenModeHelper zenModeHelper = setUpMockZenTest(); @@ -11438,7 +11422,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void onAutomaticRuleStatusChanged_sendsBroadcastToRuleOwner() throws Exception { mService.mZenModeHelper.getCallbacks().forEach(c -> c.onAutomaticRuleStatusChanged( mUserId, "rule.owner.pkg", "rule_id", AUTOMATIC_RULE_STATUS_ACTIVATED)); @@ -16302,7 +16285,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper); inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20)); - inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd(); + inOrder.verify(mPreferencesHelper).syncHasPriorityChannels(); inOrder.verifyNoMoreInteractions(); } @@ -16318,11 +16301,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper); inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20)); - inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd(); + inOrder.verify(mPreferencesHelper).syncHasPriorityChannels(); inOrder.verifyNoMoreInteractions(); } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void onUserStopped_callBackToListeners() { + Intent intent = new Intent(Intent.ACTION_USER_STOPPED); + intent.putExtra(Intent.EXTRA_USER_HANDLE, 20); + + mUserIntentReceiver.onReceive(mContext, intent); + + verify(mConditionProviders).onUserStopped(eq(20)); + verify(mListeners).onUserStopped(eq(20)); + verify(mAssistants).onUserStopped(eq(20)); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception { final String notReal = "NOT REAL"; final var checker = mService.permissionChecker; @@ -16339,6 +16336,25 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_invalidPackage_concurrent_multiUser() + throws Exception { + final String notReal = "NOT REAL"; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow( + PackageManager.NameNotFoundException.class); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt()); + verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean()); + verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_hasPermission() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16357,6 +16373,27 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_hasPermission_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true)) + .thenReturn(PackageManager.PERMISSION_GRANTED); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16375,6 +16412,27 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_isPackageAllowed_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt())) + .thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any(), anyInt()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16392,6 +16450,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_isComponentEnabled_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mListeners.isComponentEnabledForPackage(packageName, mUserId)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16408,10 +16486,30 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mDevicePolicyManager).isActiveDeviceOwner(uid); } + @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_isDeviceOwner_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + } + /** * b/292163859 */ @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16430,7 +16528,32 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid); } + /** + * b/292163859 + */ @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_callerIsDeviceOwner_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final int callingUid = Binder.getCallingUid(); + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid); + } + + @Test + @DisableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) public void isNotificationPolicyAccessGranted_notGranted() throws Exception { final String packageName = "target"; final int uid = 123; @@ -16447,6 +16570,24 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(FLAG_MANAGED_SERVICES_CONCURRENT_MULTIUSER) + public void isNotificationPolicyAccessGranted_notGranted_concurrent_multiUser() + throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName, mUserId); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + } + + @Test public void testResetDefaultDnd() { TestableNotificationManagerService service = spy(mService); UserInfo user = new UserInfo(0, "owner", 0); @@ -16481,7 +16622,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void setDeviceEffectsApplier_succeeds() throws Exception { initNMS(SystemService.PHASE_SYSTEM_SERVICES_READY); @@ -16492,7 +16632,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void setDeviceEffectsApplier_tooLate_throws() throws Exception { initNMS(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START); @@ -16501,7 +16640,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) public void setDeviceEffectsApplier_calledTwice_throws() throws Exception { initNMS(SystemService.PHASE_SYSTEM_SERVICES_READY); @@ -16513,7 +16651,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void setNotificationPolicy_mappedToImplicitRule() throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mService.setCallerIsNormalPackage(); ZenModeHelper zenHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenHelper; @@ -16530,7 +16667,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void setNotificationPolicy_systemCaller_setsGlobalPolicy() throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenModeHelper; when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt())) @@ -16570,7 +16706,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private void setNotificationPolicy_dependingOnCompanionAppDevice_maySetGlobalPolicy( @AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy) throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mService.setCallerIsNormalPackage(); ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenModeHelper; @@ -16597,7 +16732,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void setNotificationPolicy_withoutCompat_setsGlobalPolicy() throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mService.setCallerIsNormalPackage(); ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenModeHelper; @@ -16613,7 +16747,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void getNotificationPolicy_mappedFromImplicitRule() throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mService.setCallerIsNormalPackage(); ZenModeHelper zenHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenHelper; @@ -16628,7 +16761,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void setInterruptionFilter_mappedToImplicitRule() throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mService.setCallerIsNormalPackage(); ZenModeHelper zenHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenHelper; @@ -16644,7 +16776,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void setInterruptionFilter_systemCaller_setsGlobalPolicy() throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mService.setCallerIsNormalPackage(); ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenModeHelper; @@ -16683,7 +16814,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private void setInterruptionFilter_dependingOnCompanionAppDevice_maySetGlobalZen( @AssociationRequest.DeviceProfile String deviceProfile, boolean canSetGlobalPolicy) throws RemoteException { - mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = zenModeHelper; mService.setCallerIsNormalPackage(); @@ -16708,7 +16838,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void requestInterruptionFilterFromListener_fromApp_doesNotSetGlobalZen() throws Exception { @@ -16726,7 +16855,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void requestInterruptionFilterFromListener_fromSystem_setsGlobalZen() throws Exception { @@ -16745,24 +16873,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @DisableFlags(android.app.Flags.FLAG_MODES_API) - public void requestInterruptionFilterFromListener_flagOff_callsRequestFromListener() - throws Exception { - mService.setCallerIsNormalPackage(); - mService.mZenModeHelper = mock(ZenModeHelper.class); - ManagedServices.ManagedServiceInfo info = mock(ManagedServices.ManagedServiceInfo.class); - when(mListeners.checkServiceTokenLocked(any())).thenReturn(info); - info.component = new ComponentName("pkg", "cls"); - - mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class), - INTERRUPTION_FILTER_PRIORITY); - - verify(mService.mZenModeHelper).requestFromListener(eq(info.component), - eq(INTERRUPTION_FILTER_PRIORITY), eq(mUid), /* fromSystemOrSystemUi= */ eq(false)); - } - - @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void updateAutomaticZenRule_implicitRuleWithoutCPS_disallowedFromApp() throws Exception { setUpRealZenTest(); @@ -16788,7 +16898,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_MODES_API) @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void updateAutomaticZenRule_implicitRuleWithoutCPS_allowedFromSystem() throws Exception { setUpRealZenTest(); @@ -16814,7 +16923,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI}) + @EnableFlags(android.app.Flags.FLAG_MODES_UI) public void setNotificationPolicy_fromSystemApp_appliesPriorityChannelsAllowed() throws Exception { setUpRealZenTest(); @@ -16844,7 +16953,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI}) + @EnableFlags(android.app.Flags.FLAG_MODES_UI) @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES) public void setNotificationPolicy_fromRegularAppThatCanModifyPolicy_ignoresState() throws Exception { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 640de174ba20..5dea44d6ebf4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -363,7 +363,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { .when(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI)); mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); + NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0); when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); when(mAppOpsManager.noteOpNoThrow(anyInt(), anyInt(), anyString(), eq(null), anyString())).thenReturn(MODE_DEFAULT); @@ -2733,7 +2733,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { @@ -2748,7 +2748,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(true); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, uid, false); - assertTrue(mHelper.areChannelsBypassingDnd()); + assertTrue(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(true)); @@ -2760,7 +2760,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // delete channels mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false); - assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND + assertTrue(mHelper.hasPriorityChannels()); // channel2 can still bypass DND if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { @@ -2770,7 +2770,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { resetZenModeHelper(); mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(false)); @@ -2792,7 +2792,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { @@ -2807,7 +2807,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.createNotificationChannel(PKG_N_MR1, uid, update, true, true, uid, false); - assertTrue(mHelper.areChannelsBypassingDnd()); + assertTrue(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(true)); @@ -2829,7 +2829,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { @@ -2844,7 +2844,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(true); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, uid, false); - assertTrue(mHelper.areChannelsBypassingDnd()); + assertTrue(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(true)); @@ -2856,7 +2856,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // delete channels mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false); - assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND + assertTrue(mHelper.hasPriorityChannels()); // channel2 can still bypass DND if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { @@ -2866,7 +2866,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { resetZenModeHelper(); mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(false)); @@ -2884,9 +2884,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start in a 'allowed to bypass dnd state' mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); + NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0); when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); - mHelper.syncChannelsBypassingDnd(); + mHelper.syncHasPriorityChannels(); // create notification channel that can bypass dnd, but app is blocked // expected result: areChannelsBypassingDnd = false @@ -2899,7 +2899,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(true); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(false)); @@ -2917,9 +2917,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start in a 'allowed to bypass dnd state' mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); + NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0); when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); - mHelper.syncChannelsBypassingDnd(); + mHelper.syncHasPriorityChannels(); // create notification channel that can bypass dnd, but app is blocked // expected result: areChannelsBypassingDnd = false @@ -2927,7 +2927,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(true); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(false)); @@ -2945,9 +2945,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start in a 'allowed to bypass dnd state' mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); + NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0); when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); - mHelper.syncChannelsBypassingDnd(); + mHelper.syncHasPriorityChannels(); // create notification channel that can bypass dnd, but app is blocked // expected result: areChannelsBypassingDnd = false @@ -2955,7 +2955,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(true); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true, uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(false)); @@ -2977,7 +2977,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { NotificationChannel channel = new NotificationChannel("id1", "name1", IMPORTANCE_LOW); mHelper.createNotificationChannel(PKG_N_MR1, uid, channel, true, false, uid, false); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { @@ -2990,7 +2990,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = true channel.setBypassDnd(true); mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true); - assertTrue(mHelper.areChannelsBypassingDnd()); + assertTrue(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(true)); @@ -3004,7 +3004,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // expected result: areChannelsBypassingDnd = false channel.setBypassDnd(false); mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(false)); @@ -3020,10 +3020,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start notification policy off with mAreChannelsBypassingDnd = true, but // RankingHelper should change to false mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); + NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0); when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); - mHelper.syncChannelsBypassingDnd(); - assertFalse(mHelper.areChannelsBypassingDnd()); + mHelper.syncHasPriorityChannels(); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), eq(false)); @@ -3039,7 +3039,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start notification policy off with mAreChannelsBypassingDnd = false mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0, 0); when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); - assertFalse(mHelper.areChannelsBypassingDnd()); + assertFalse(mHelper.hasPriorityChannels()); if (android.app.Flags.modesUi()) { verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { @@ -3050,7 +3050,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - public void syncChannelsBypassingDnd_includesProfilesOfCurrentUser() throws Exception { + public void syncHasPriorityChannels_includesProfilesOfCurrentUser() throws Exception { when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0, 10})); when(mPermissionHelper.hasPermission(anyInt())).thenReturn(true); ApplicationInfo appInfo = new ApplicationInfo(); @@ -3067,13 +3067,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.createNotificationChannel("com.example", UserHandle.getUid(10, 444), withBypass, false, false, Process.SYSTEM_UID, true); - mHelper.syncChannelsBypassingDnd(); + mHelper.syncHasPriorityChannels(); - assertThat(mHelper.areChannelsBypassingDnd()).isTrue(); + assertThat(mHelper.hasPriorityChannels()).isTrue(); } @Test - public void syncChannelsBypassingDnd_excludesOtherUsers() throws Exception { + public void syncHasPriorityChannels_excludesOtherUsers() throws Exception { when(mUserProfiles.getCurrentProfileIds()).thenReturn(IntArray.wrap(new int[] {0})); when(mPermissionHelper.hasPermission(anyInt())).thenReturn(true); ApplicationInfo appInfo = new ApplicationInfo(); @@ -3090,9 +3090,9 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.createNotificationChannel("com.example", UserHandle.getUid(10, 444), withBypass, false, false, Process.SYSTEM_UID, true); - mHelper.syncChannelsBypassingDnd(); + mHelper.syncHasPriorityChannels(); - assertThat(mHelper.areChannelsBypassingDnd()).isFalse(); + assertThat(mHelper.hasPriorityChannels()).isFalse(); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index f90034614383..ec428d506e7b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -17,9 +17,10 @@ package com.android.server.notification; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_LOW; - import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; + import static com.google.common.truth.Truth.assertThat; + import static junit.framework.TestCase.assertEquals; import static org.junit.Assert.assertTrue; @@ -29,7 +30,6 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import android.app.Flags; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -40,7 +40,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; -import android.media.AudioAttributes; import android.net.Uri; import android.os.Build; import android.os.UserHandle; @@ -67,7 +66,6 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collections; -import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) @@ -154,7 +152,7 @@ public class RankingHelperTest extends UiServiceTestCase { .thenReturn(SOUND_URI); mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, - NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); + NotificationManager.Policy.STATE_HAS_PRIORITY_CHANNELS, 0); when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper, mUsageStats, new String[] {ImportanceExtractor.class.getName()}, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java index 75552bc433c5..f3813437a9c5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java @@ -20,10 +20,7 @@ import static android.service.notification.ZenAdapters.notificationPolicyToZenPo import static com.google.common.truth.Truth.assertThat; -import android.app.Flags; import android.app.NotificationManager.Policy; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenPolicy; @@ -137,8 +134,7 @@ public class ZenAdaptersTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void notificationPolicyToZenPolicy_modesApi_priorityChannels() { + public void notificationPolicyToZenPolicy_priorityChannels() { Policy policy = new Policy(0, 0, 0, 0, Policy.policyState(false, true), 0); @@ -151,20 +147,4 @@ public class ZenAdaptersTest extends UiServiceTestCase { assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo( ZenPolicy.STATE_DISALLOW); } - - @Test - @DisableFlags(Flags.FLAG_MODES_API) - public void notificationPolicyToZenPolicy_noModesApi_priorityChannelsUnset() { - Policy policy = new Policy(0, 0, 0, 0, - Policy.policyState(false, true), 0); - - ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); - assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); - - Policy notAllowed = new Policy(0, 0, 0, 0, - Policy.policyState(false, false), 0); - ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); - assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo( - ZenPolicy.STATE_UNSET); - } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java index af911e811e5e..9a2b748a9bcc 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java @@ -20,7 +20,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import android.app.Flags; import android.os.Parcel; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenDeviceEffects; @@ -31,7 +30,6 @@ import com.android.server.UiServiceTestCase; import com.google.common.collect.ImmutableSet; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,11 +40,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Before - public final void setUp() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - } - @Test public void builder() { ZenDeviceEffects deviceEffects = diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index b42a6a5a7382..67efb9e76692 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -174,7 +174,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { } assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config)); - config.areChannelsBypassingDnd = true; + config.hasPriorityChannels = true; assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config)); if (Flags.modesUi()) { @@ -187,7 +187,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertFalse(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config)); - config.areChannelsBypassingDnd = false; + config.hasPriorityChannels = false; if (Flags.modesUi()) { config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy) .allowPriorityChannels(false) @@ -417,7 +417,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertTrue(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config)); assertTrue(ZenModeConfig.areAllZenBehaviorSoundsMuted(config)); - config.areChannelsBypassingDnd = true; + config.hasPriorityChannels = true; if (Flags.modesUi()) { config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy) .allowPriorityChannels(true) @@ -429,7 +429,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertFalse(ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(config)); assertFalse(ZenModeConfig.areAllZenBehaviorSoundsMuted(config)); - config.areChannelsBypassingDnd = false; + config.hasPriorityChannels = false; if (Flags.modesUi()) { config.manualRule.zenPolicy = new ZenPolicy.Builder(config.manualRule.zenPolicy) .allowPriorityChannels(false) @@ -488,33 +488,33 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = null; rule.zenDeviceEffects = null; - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.userModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_policyModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = new ZenPolicy(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenPolicyUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build(); - assertThat(rule.canBeUpdatedByApp()).isTrue(); + assertThat(rule.isUserModified()).isFalse(); rule.zenDeviceEffectsUserModifiedFields = 1; - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); } @Test @@ -548,7 +548,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.creationTime = 123; rule.id = "id"; rule.zenMode = INTERRUPTION_FILTER; - rule.modified = true; rule.name = NAME; rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); @@ -564,6 +563,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(456); + } } config.automaticRules.put(rule.id, rule); @@ -585,7 +587,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.condition, ruleActual.condition); assertEquals(rule.enabled, ruleActual.enabled); assertEquals(rule.creationTime, ruleActual.creationTime); - assertEquals(rule.modified, ruleActual.modified); assertEquals(rule.conditionId, ruleActual.conditionId); assertEquals(rule.name, ruleActual.name); assertEquals(rule.zenMode, ruleActual.zenMode); @@ -602,6 +603,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, ruleActual.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, ruleActual.lastActivation); + } } if (Flags.backupRestoreLogging()) { verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2); @@ -620,7 +624,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.creationTime = 123; rule.id = "id"; rule.zenMode = INTERRUPTION_FILTER; - rule.modified = true; rule.name = NAME; rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); @@ -636,6 +639,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(789); + } } Parcel parcel = Parcel.obtain(); @@ -651,7 +657,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.condition, parceled.condition); assertEquals(rule.enabled, parceled.enabled); assertEquals(rule.creationTime, parceled.creationTime); - assertEquals(rule.modified, parceled.modified); assertEquals(rule.conditionId, parceled.conditionId); assertEquals(rule.name, parceled.name); assertEquals(rule.zenMode, parceled.zenMode); @@ -668,6 +673,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, parceled.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, parceled.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, parceled.lastActivation); + } } assertEquals(rule, parceled); @@ -685,7 +693,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.creationTime = 123; rule.id = "id"; rule.zenMode = Settings.Global.ZEN_MODE_ALARMS; - rule.modified = true; rule.name = "name"; rule.snoozing = true; rule.pkg = "b"; @@ -705,7 +712,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.condition, fromXml.condition); assertEquals(rule.enabled, fromXml.enabled); assertEquals(rule.creationTime, fromXml.creationTime); - assertEquals(rule.modified, fromXml.modified); assertEquals(rule.conditionId, fromXml.conditionId); assertEquals(rule.name, fromXml.name); assertEquals(rule.zenMode, fromXml.zenMode); @@ -721,7 +727,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.enabled = ENABLED; rule.id = "id"; rule.zenMode = INTERRUPTION_FILTER; - rule.modified = true; rule.name = NAME; rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); @@ -753,6 +758,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); if (Flags.modesUi()) { rule.disabledOrigin = ZenModeConfig.ORIGIN_APP; + if (Flags.modesCleanupImplicit()) { + rule.lastActivation = Instant.ofEpochMilli(123); + } } ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -770,7 +778,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.condition, fromXml.condition); assertEquals(rule.enabled, fromXml.enabled); assertEquals(rule.creationTime, fromXml.creationTime); - assertEquals(rule.modified, fromXml.modified); assertEquals(rule.conditionId, fromXml.conditionId); assertEquals(rule.name, fromXml.name); assertEquals(rule.zenMode, fromXml.zenMode); @@ -789,6 +796,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.deletionInstant, fromXml.deletionInstant); if (Flags.modesUi()) { assertEquals(rule.disabledOrigin, fromXml.disabledOrigin); + if (Flags.modesCleanupImplicit()) { + assertEquals(rule.lastActivation, fromXml.lastActivation); + } } } @@ -916,7 +926,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME; assertThat(rule.userModifiedFields).isEqualTo(1); - assertThat(rule.canBeUpdatedByApp()).isFalse(); + assertThat(rule.isUserModified()).isTrue(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); writeRuleXml(rule, baos); @@ -924,7 +934,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule fromXml = readRuleXml(bais); assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields); - assertThat(fromXml.canBeUpdatedByApp()).isFalse(); + assertThat(fromXml.isUserModified()).isTrue(); } @Test @@ -1259,7 +1269,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.creationTime = 123; rule.id = "id"; rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - rule.modified = true; rule.name = "name"; rule.pkg = "b"; config.automaticRules.put("key", rule); @@ -1348,7 +1357,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { config.setSuppressedVisualEffects(0); config.setAllowPriorityChannels(false); } - config.areChannelsBypassingDnd = false; + config.hasPriorityChannels = false; return config; } @@ -1383,7 +1392,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { config.setSuppressedVisualEffects(0); config.setAllowPriorityChannels(true); } - config.areChannelsBypassingDnd = false; + config.hasPriorityChannels = false; return config; } @@ -1410,7 +1419,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { config.setAllowConversationsFrom(CONVERSATION_SENDERS_NONE); config.setSuppressedVisualEffects(0); } - config.areChannelsBypassingDnd = false; + config.hasPriorityChannels = false; return config; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index b138c72875a6..6d0bf8b322fd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -16,7 +16,6 @@ package com.android.server.notification; -import static android.app.Flags.FLAG_MODES_API; import static android.app.Flags.FLAG_MODES_UI; import static com.google.common.truth.Truth.assertThat; @@ -64,7 +63,6 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; @@ -78,20 +76,14 @@ public class ZenModeDiffTest extends UiServiceTestCase { // version is not included in the diff; manual & automatic rules have special handling; // deleted rules are not included in the diff. public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS = - android.app.Flags.modesApi() - ? Set.of("version", "manualRule", "automaticRules", "deletedRules") - : Set.of("version", "manualRule", "automaticRules"); - - // allowPriorityChannels is flagged by android.app.modes_api - public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS = - Set.of("allowPriorityChannels"); + Set.of("version", "manualRule", "automaticRules", "deletedRules"); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.progressionOf(FLAG_MODES_API, FLAG_MODES_UI); + return FlagsParameterization.progressionOf(FLAG_MODES_UI); } public ZenModeDiffTest(FlagsParameterization flags) { @@ -147,7 +139,7 @@ public class ZenModeDiffTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void testRuleDiff_toStringNoChangeAddRemove() throws Exception { // Start with two identical rules ZenModeConfig.ZenRule r1 = makeRule(); @@ -164,7 +156,7 @@ public class ZenModeDiffTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void testRuleDiff_toString() throws Exception { // Start with two identical rules ZenModeConfig.ZenRule r1 = makeRule(); @@ -218,7 +210,6 @@ public class ZenModeDiffTest extends UiServiceTestCase { + "mPriorityCalls:2->1, " + "mConversationSenders:2->1, " + "mAllowChannels:2->1}, " - + "modified:true->false, " + "pkg:string1->string2, " + "zenDeviceEffects:ZenDeviceEffectsDiff{" + "mGrayscale:true->false, " @@ -241,7 +232,7 @@ public class ZenModeDiffTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void testRuleDiff_toStringNullStartPolicy() throws Exception { // Start with two identical rules ZenModeConfig.ZenRule r1 = makeRule(); @@ -278,7 +269,6 @@ public class ZenModeDiffTest extends UiServiceTestCase { + "creationTime:200->100, " + "enabler:string1->string2, " + "zenPolicy:ZenPolicyDiff{added}, " - + "modified:true->false, " + "pkg:string1->string2, " + "zenDeviceEffects:ZenDeviceEffectsDiff{added}, " + "triggerDescription:string1->string2, " @@ -485,16 +475,10 @@ public class ZenModeDiffTest extends UiServiceTestCase { // "Metadata" fields are never compared. Set<String> exemptFields = new LinkedHashSet<>( Set.of("userModifiedFields", "zenPolicyUserModifiedFields", - "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin")); + "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin", + "lastActivation")); // Flagged fields are only compared if their flag is on. - if (!Flags.modesApi()) { - exemptFields.addAll( - Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION, - RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL, - RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, - RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS)); - } - if (Flags.modesApi() && Flags.modesUi()) { + if (Flags.modesUi()) { exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete. } else { exemptFields.add(RuleDiff.FIELD_CONDITION_OVERRIDE); @@ -530,35 +514,6 @@ public class ZenModeDiffTest extends UiServiceTestCase { ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); ArrayMap<String, Object> expectedTo = new ArrayMap<>(); List<Field> fieldsForDiff = getFieldsForDiffCheck( - ZenModeConfig.class, getConfigExemptAndFlaggedFields(), false); - generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo); - - ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2); - assertTrue(d.hasDiff()); - - // Now diff them and check that each of the fields has a diff - for (Field f : fieldsForDiff) { - String name = f.getName(); - assertNotNull("diff not found for field: " + name, d.getDiffForField(name)); - assertTrue(d.getDiffForField(name).hasDiff()); - assertTrue("unexpected field: " + name, expectedFrom.containsKey(name)); - assertTrue("unexpected field: " + name, expectedTo.containsKey(name)); - assertEquals(expectedFrom.get(name), d.getDiffForField(name).from()); - assertEquals(expectedTo.get(name), d.getDiffForField(name).to()); - } - } - - @Test - @EnableFlags(FLAG_MODES_API) - public void testConfigDiff_fieldDiffs_flagOn() throws Exception { - // these two start the same - ZenModeConfig c1 = new ZenModeConfig(); - ZenModeConfig c2 = new ZenModeConfig(); - - // maps mapping field name -> expected output value as we set diffs - ArrayMap<String, Object> expectedFrom = new ArrayMap<>(); - ArrayMap<String, Object> expectedTo = new ArrayMap<>(); - List<Field> fieldsForDiff = getFieldsForDiffCheck( ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS, false); generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo); @@ -656,14 +611,6 @@ public class ZenModeDiffTest extends UiServiceTestCase { assertEquals("different", automaticDiffs.get("ruleId").getDiffForField("pkg").to()); } - // Helper method that merges the base exempt fields with fields that are flagged - private Set getConfigExemptAndFlaggedFields() { - Set merged = new HashSet(); - merged.addAll(ZEN_MODE_CONFIG_EXEMPT_FIELDS); - merged.addAll(ZEN_MODE_CONFIG_FLAGGED_FIELDS); - return merged; - } - // Helper methods for working with configs, policies, rules // Just makes a zen rule with fields filled in private ZenModeConfig.ZenRule makeRule() { @@ -676,20 +623,17 @@ public class ZenModeDiffTest extends UiServiceTestCase { rule.creationTime = 123; rule.id = "ruleId"; rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; - rule.modified = false; rule.name = "name"; rule.setConditionOverride(ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE); rule.pkg = "a"; - if (android.app.Flags.modesApi()) { - rule.allowManualInvocation = true; - rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME; - rule.iconResName = "res"; - rule.triggerDescription = "At night"; - rule.zenDeviceEffects = new ZenDeviceEffects.Builder() - .setShouldDimWallpaper(true) - .build(); - rule.userModifiedFields = AutomaticZenRule.FIELD_NAME; - } + rule.allowManualInvocation = true; + rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME; + rule.iconResName = "res"; + rule.triggerDescription = "At night"; + rule.zenDeviceEffects = new ZenDeviceEffects.Builder() + .setShouldDimWallpaper(true) + .build(); + rule.userModifiedFields = AutomaticZenRule.FIELD_NAME; return rule; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java index a49f5a89b11b..2f0b3ecb593a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java @@ -37,7 +37,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import android.app.Flags; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager.Policy; @@ -492,8 +491,6 @@ public class ZenModeFilteringTest extends UiServiceTestCase { @Test public void testAllowChannels_priorityPackage() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - // Notification with package priority = PRIORITY_MAX (assigned to indicate canBypassDnd) NotificationRecord r = getNotificationRecord(); r.setPackagePriority(Notification.PRIORITY_MAX); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 31b9cf72584c..0ab11e0cbe3d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -23,7 +23,7 @@ import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_THEATER; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING; -import static android.app.Flags.FLAG_MODES_API; +import static android.app.Flags.FLAG_MODES_CLEANUP_IMPLICIT; import static android.app.Flags.FLAG_MODES_MULTIUSER; import static android.app.Flags.FLAG_MODES_UI; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; @@ -85,6 +85,7 @@ import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_PEEK; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS; +import static com.android.os.dnd.DNDProtoEnums.CONV_IMPORTANT; import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED; import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG; import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; @@ -124,7 +125,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import static java.time.temporal.ChronoUnit.DAYS; + import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AlarmManager; @@ -219,11 +223,11 @@ import java.io.Reader; import java.io.StringWriter; import java.time.Instant; import java.time.ZoneOffset; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Calendar; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -713,7 +717,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testTotalSilence_consolidatedPolicyDisallowsAll() { // Start with zen mode off just to make sure global/manual mode isn't doing anything. mZenModeHelper.mZenMode = ZEN_MODE_OFF; @@ -746,7 +749,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testAlarmsOnly_consolidatedPolicyOnlyAllowsAlarmsAndMedia() { // Start with zen mode off just to make sure global/manual mode isn't doing anything. mZenModeHelper.mZenMode = ZEN_MODE_OFF; @@ -1136,8 +1138,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testProto() throws InvalidProtocolBufferException { mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, null, - "test", CUSTOM_PKG_UID); + ORIGIN_USER_IN_SYSTEMUI, null, "test", CUSTOM_PKG_UID); mZenModeHelper.mConfig.automaticRules = new ArrayMap<>(); // no automatic rules @@ -1262,7 +1263,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testProtoWithAutoRuleCustomPolicy() throws Exception { setupZenConfig(); // clear any automatic rules just to make sure @@ -1304,7 +1304,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testProtoWithAutoRuleWithModifiedFields() throws Exception { setupZenConfig(); mZenModeHelper.mConfig.automaticRules = new ArrayMap<>(); @@ -2005,7 +2004,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testReadXml_onModesApi_noUpgrade() throws Exception { // When reading XML for something that is already on the modes API system, make sure no // rules' policies get changed. @@ -2053,7 +2051,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testReadXml_upgradeToModesApi_makesCustomPolicies() throws Exception { // When reading in an XML file written from a pre-modes-API version, confirm that we create // a custom policy matching the global config for any automatic rule with no specified @@ -2105,7 +2102,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testReadXml_upgradeToModesApi_fillsInCustomPolicies() throws Exception { // When reading in an XML file written from a pre-modes-API version, confirm that for an // underspecified ZenPolicy, we fill in all of the gaps with things from the global config @@ -2165,7 +2161,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testReadXml_upgradeToModesApi_existingDefaultRulesGetCustomPolicy() throws Exception { setupZenConfig(); @@ -2227,7 +2222,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void testReadXml_upgradeToModesUi_resetsImplicitRuleIcon() throws Exception { setupZenConfig(); mZenModeHelper.mConfig.automaticRules.clear(); @@ -2241,13 +2236,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id, implicitRuleBeforeModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); - // Write with pre-modes-ui = (modes_api) version, then re-read. + // Write with pre-modes-ui version, then re-read. ByteArrayOutputStream baos = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_API); TypedXmlPullParser parser = Xml.newFastPullParser(); parser.setInput(new BufferedInputStream( @@ -2265,7 +2259,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void testReadXml_onModesUi_implicitRulesUntouched() throws Exception { setupZenConfig(); mZenModeHelper.mConfig.automaticRules.clear(); @@ -2279,8 +2273,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { implicitRuleWithModesUi); // Plus one other normal rule. - ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null); - anotherRule.id = "other_rule"; + ZenRule anotherRule = newZenRule("other_rule", "other_pkg", Instant.now()); anotherRule.iconResName = "other_icon"; anotherRule.type = TYPE_IMMERSIVE; mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule); @@ -2342,7 +2335,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // shouldn't update rule that's been modified ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule(); - updatedDefaultRule.modified = true; + updatedDefaultRule.userModifiedFields = AutomaticZenRule.FIELD_NAME; updatedDefaultRule.enabled = false; updatedDefaultRule.creationTime = 0; updatedDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID; @@ -2370,8 +2363,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // will update rule that is not enabled and modified ZenModeConfig.ZenRule customDefaultRule = new ZenModeConfig.ZenRule(); customDefaultRule.pkg = SystemZenRules.PACKAGE_ANDROID; + customDefaultRule.userModifiedFields = AutomaticZenRule.FIELD_NAME; customDefaultRule.enabled = false; - customDefaultRule.modified = false; customDefaultRule.creationTime = 0; customDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID; customDefaultRule.name = "Schedule Default Rule"; @@ -2391,7 +2384,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.ZenRule ruleAfterUpdating = mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID); assertEquals(customDefaultRule.enabled, ruleAfterUpdating.enabled); - assertEquals(customDefaultRule.modified, ruleAfterUpdating.modified); + assertEquals(customDefaultRule.userModifiedFields, ruleAfterUpdating.userModifiedFields); assertEquals(customDefaultRule.id, ruleAfterUpdating.id); assertEquals(customDefaultRule.conditionId, ruleAfterUpdating.conditionId); assertNotEquals(defaultRuleName, ruleAfterUpdating.name); // update name @@ -2401,8 +2394,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) - public void testDefaultRulesFromConfig_modesApi_getPolicies() { + public void testDefaultRulesFromConfig_getPolicies() { // After mZenModeHelper was created, set some things in the policy so it's changed from // default. setupZenConfig(); @@ -2530,7 +2522,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertTrue(ruleInConfig != null); assertEquals(zenRule.isEnabled(), ruleInConfig.enabled); - assertEquals(zenRule.isModified(), ruleInConfig.modified); assertEquals(zenRule.getConditionId(), ruleInConfig.conditionId); assertEquals(NotificationManager.zenModeFromInterruptionFilter( zenRule.getInterruptionFilter(), -1), ruleInConfig.zenMode); @@ -2551,7 +2542,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertTrue(ruleInConfig != null); assertEquals(zenRule.isEnabled(), ruleInConfig.enabled); - assertEquals(zenRule.isModified(), ruleInConfig.modified); assertEquals(zenRule.getConditionId(), ruleInConfig.conditionId); assertEquals(NotificationManager.zenModeFromInterruptionFilter( zenRule.getInterruptionFilter(), -1), ruleInConfig.zenMode); @@ -2560,8 +2550,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) - public void testAddAutomaticZenRule_modesApi_fillsInDefaultValues() { + public void testAddAutomaticZenRule_fillsInDefaultValues() { // When a new automatic zen rule is added with only some fields filled in, ensure that // all unset fields are filled in with device defaults. @@ -2762,7 +2751,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_fromApp_ignoresHiddenEffects() { ZenDeviceEffects zde = new ZenDeviceEffects.Builder() @@ -2799,7 +2787,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_fromSystem_respectsHiddenEffects() { ZenDeviceEffects zde = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -2828,7 +2815,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_fromUser_respectsHiddenEffects() throws Exception { ZenDeviceEffects zde = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -2859,7 +2845,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_fromApp_preservesPreviousHiddenEffects() { ZenDeviceEffects original = new ZenDeviceEffects.Builder() .setShouldDisableTapToWake(true) @@ -2896,7 +2881,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_fromSystem_updatesHiddenEffects() { ZenDeviceEffects original = new ZenDeviceEffects.Builder() .setShouldDisableTapToWake(true) @@ -2925,7 +2909,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_fromUser_updatesHiddenEffects() { ZenDeviceEffects original = new ZenDeviceEffects.Builder() .setShouldDisableTapToWake(true) @@ -2958,7 +2941,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_nullPolicy_doesNothing() { // Test that when updateAutomaticZenRule is called with a null policy, nothing changes // about the existing policy. @@ -2985,7 +2967,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_overwritesExistingPolicy() { // Test that when updating an automatic zen rule with an existing policy, the newly set // fields overwrite those from the previous policy, but unset fields in the new policy @@ -3024,7 +3005,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() { ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); @@ -3044,7 +3024,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_withTypeBedtime_keepsEnabledSleeping() { ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); @@ -3065,7 +3044,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_withTypeBedtime_keepsCustomizedSleeping() { ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); @@ -3086,7 +3064,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() { ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); @@ -3113,7 +3091,34 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) + public void getAutomaticZenRules_returnsOwnedRules() { + AutomaticZenRule myRule1 = new AutomaticZenRule.Builder("My Rule 1", Uri.parse("1")) + .setPackage(mPkg) + .setConfigurationActivity(new ComponentName(mPkg, "myActivity")) + .build(); + AutomaticZenRule myRule2 = new AutomaticZenRule.Builder("My Rule 2", Uri.parse("2")) + .setPackage(mPkg) + .setConfigurationActivity(new ComponentName(mPkg, "myActivity")) + .build(); + AutomaticZenRule otherPkgRule = new AutomaticZenRule.Builder("Other", Uri.parse("3")) + .setPackage("com.other.package") + .setConfigurationActivity(new ComponentName("com.other.package", "theirActivity")) + .build(); + + String rule1Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, myRule1, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String rule2Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, myRule2, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String otherRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + "com.other.package", otherPkgRule, ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + Map<String, AutomaticZenRule> rules = mZenModeHelper.getAutomaticZenRules( + UserHandle.CURRENT, CUSTOM_PKG_UID); + + assertThat(rules.keySet()).containsExactly(rule1Id, rule2Id); + } + + @Test public void testSetManualZenMode() { setupZenConfig(); @@ -3133,7 +3138,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) @DisableFlags(FLAG_MODES_UI) public void setManualZenMode_off_snoozesActiveRules() { for (ZenChangeOrigin origin : ZenChangeOrigin.values()) { @@ -3172,7 +3176,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setManualZenMode_off_doesNotSnoozeRulesIfFromUserInSystemUi() { for (ZenChangeOrigin origin : ZenChangeOrigin.values()) { // Start with an active rule and an inactive rule @@ -3246,7 +3250,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn zen mode on (to important_interruptions) // Need to additionally call the looper in order to finish the post-apply-config process mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", null, SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, "", null, SYSTEM_UID); // Now turn zen mode off, but via a different package UID -- this should get registered as // "not an action by the user" because some other app is changing zen mode @@ -3273,14 +3277,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0)); assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); - assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo( - !(Flags.modesUi() || Flags.modesApi())); + assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isFalse(); assertTrue(mZenModeEventLogger.getIsUserAction(0)); assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); // change origin should be populated only under modes_ui assertThat(mZenModeEventLogger.getChangeOrigin(0)).isEqualTo( - (Flags.modesApi() && Flags.modesUi()) ? ORIGIN_USER_IN_SYSTEMUI : 0); + (Flags.modesUi()) ? ORIGIN_USER_IN_SYSTEMUI : 0); // and from turning zen mode off: // - event ID: DND_TURNED_OFF @@ -3298,11 +3301,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(0, mZenModeEventLogger.getNumRulesActive(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); - if (Flags.modesApi()) { - assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull(); - } else { - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); - } + assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull(); assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( Flags.modesUi() ? ORIGIN_APP : 0); } @@ -3334,8 +3333,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Event 2: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, id, zenRule, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", - SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID); AutomaticZenRule systemRule = new AutomaticZenRule("systemRule", null, @@ -3345,8 +3343,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String systemId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), systemRule, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "test", - SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, "test", SYSTEM_UID); // Event 3: turn on the system rule mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, systemId, @@ -3355,8 +3352,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Event 4: "User" deletes the rule mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, systemId, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", - SYSTEM_UID); + ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID); // In total, this represents 4 events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3394,11 +3390,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertTrue(mZenModeEventLogger.getIsUserAction(1)); assertThat(mZenModeEventLogger.getPackageUid(1)).isEqualTo( Flags.modesUi() ? CUSTOM_PKG_UID : SYSTEM_UID); - if (Flags.modesApi()) { - assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull(); - } else { - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); - } + assertThat(mZenModeEventLogger.getPolicyProto(1)).isNull(); assertThat(mZenModeEventLogger.getChangeOrigin(1)).isEqualTo( Flags.modesUi() ? ORIGIN_USER_IN_SYSTEMUI : 0); @@ -3426,7 +3418,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testZenModeEventLog_automaticRuleActivatedFromAppByAppAndUser() throws IllegalArgumentException { mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); @@ -3816,13 +3807,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Now change apps bypassing to true ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); - newConfig.areChannelsBypassingDnd = true; + newConfig.hasPriorityChannels = true; mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(), ORIGIN_SYSTEM, SYSTEM_UID); assertEquals(2, mZenModeEventLogger.numLoggedChanges()); // and then back to false, all without changing anything else - newConfig.areChannelsBypassingDnd = false; + newConfig.hasPriorityChannels = false; mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(), ORIGIN_SYSTEM, SYSTEM_UID); assertEquals(3, mZenModeEventLogger.numLoggedChanges()); @@ -3869,10 +3860,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testZenModeEventLog_policyAllowChannels() { - // when modes_api flag is on, ensure that any change in allow_channels gets logged, - // even when there are no other changes. + // Ensure that any change in allow_channels gets logged, even when there are no other + // changes. mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); // Default zen config has allow channels = priority (aka on) @@ -3919,7 +3909,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testZenModeEventLog_ruleWithInterruptionFilterAll_notLoggedAsDndChange() { mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -3961,7 +3950,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testZenModeEventLog_activeRuleTypes() { mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -4050,43 +4038,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @DisableFlags(FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() { - setupZenConfig(); - // When there's one automatic rule active and it doesn't specify a policy, test that the - // resulting consolidated policy is one that matches the default rule settings. - AutomaticZenRule zenRule = new AutomaticZenRule("name", - null, - new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), - ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - null, - NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); - - // enable the rule - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ORIGIN_SYSTEM, SYSTEM_UID); - - assertEquals(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT), - mZenModeHelper.getConsolidatedNotificationPolicy()); - - // inspect the consolidated policy. Based on setupZenConfig() values. - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowAlarms()); - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowMedia()); - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem()); - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders()); - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowCalls()); - assertEquals(PRIORITY_SENDERS_STARRED, mZenModeHelper.mConsolidatedPolicy.allowCallsFrom()); - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages()); - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations()); - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()); - assertFalse(mZenModeHelper.mConsolidatedPolicy.showBadges()); - } - - @Test - public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDefault() { + public void testUpdateConsolidatedPolicy_defaultRulesOnly_takesDefault() { setupZenConfig(); // When there's one automatic rule active and it doesn't specify a policy, test that the @@ -4113,53 +4065,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @DisableFlags(FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_preModesApiCustomPolicyOnly_fillInWithGlobal() { - setupZenConfig(); - - // when there's only one automatic rule active and it has a custom policy, make sure that's - // what the consolidated policy reflects whether or not it's stricter than what the global - // config would specify. - ZenPolicy customPolicy = new ZenPolicy.Builder() - .allowAlarms(true) // more lenient than default - .allowMedia(true) // more lenient than default - .allowRepeatCallers(false) // more restrictive than default - .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default - .showBadges(true) // more lenient - .showPeeking(false) // more restrictive - .build(); - - AutomaticZenRule zenRule = new AutomaticZenRule("name", - null, - new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), - ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - customPolicy, - NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); - - // enable the rule; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); - - // since this is the only active rule, the consolidated policy should match the custom - // policy for every field specified, and take default values (from device default or - // manual policy) for unspecified things - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowAlarms()); // custom - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMedia()); // custom - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem()); // default - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders()); // default - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowCalls()); // custom - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages()); // default - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations()); // default - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()); // custom - assertTrue(mZenModeHelper.mConsolidatedPolicy.showBadges()); // custom - assertFalse(mZenModeHelper.mConsolidatedPolicy.showPeeking()); // custom - } - - @Test - @EnableFlags(FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDefault() { + public void testUpdateConsolidatedPolicy_customPolicyOnly_fillInWithDefault() { setupZenConfig(); // when there's only one automatic rule active and it has a custom policy, make sure that's @@ -4204,68 +4110,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @DisableFlags(FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_preModesApiDefaultAndCustomActive_mergesWithGlobal() { - setupZenConfig(); - - // when there are two rules active, one inheriting the default policy and one setting its - // own custom policy, they should be merged to form the most restrictive combination. - - // rule 1: no custom policy - AutomaticZenRule zenRule = new AutomaticZenRule("name", - null, - new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), - ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - null, - NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); - - // enable rule 1 - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); - - // custom policy for rule 2 - ZenPolicy customPolicy = new ZenPolicy.Builder() - .allowAlarms(true) // more lenient than default - .allowMedia(true) // more lenient than default - .allowRepeatCallers(false) // more restrictive than default - .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default - .showBadges(true) // more lenient - .showPeeking(false) // more restrictive - .build(); - - AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", - null, - new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), - ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - customPolicy, - NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, - mContext.getPackageName(), zenRule2, ORIGIN_SYSTEM, "test", SYSTEM_UID); - - // enable rule 2; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, - new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - ORIGIN_SYSTEM, SYSTEM_UID); - - // now both rules should be on, and the consolidated policy should reflect the most - // restrictive option of each of the two - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowAlarms()); // default stricter - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowMedia()); // default stricter - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem()); // default, unset in custom - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowReminders()); // default - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowCalls()); // custom stricter - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMessages()); // default, unset in custom - assertTrue(mZenModeHelper.mConsolidatedPolicy.allowConversations()); // default - assertFalse(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()); // custom stricter - assertFalse(mZenModeHelper.mConsolidatedPolicy.showBadges()); // default stricter - assertFalse(mZenModeHelper.mConsolidatedPolicy.showPeeking()); // custom stricter - } - - @Test - @EnableFlags(FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() { + public void testUpdateConsolidatedPolicy_defaultAndCustomActive_mergesWithDefault() { setupZenConfig(); // when there are two rules active, one inheriting the default policy and one setting its @@ -4328,7 +4173,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testUpdateConsolidatedPolicy_allowChannels() { setupZenConfig(); @@ -4377,7 +4221,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll() { setupZenConfig(); @@ -4428,7 +4271,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void zenRuleToAutomaticZenRule_allFields() { when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( new String[]{OWNER.getPackageName()}); @@ -4442,7 +4284,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.creationTime = 123; rule.id = "id"; rule.zenMode = INTERRUPTION_FILTER_ZR; - rule.modified = true; rule.name = NAME; rule.setConditionOverride(OVERRIDE_DEACTIVATE); rule.pkg = OWNER.getPackageName(); @@ -4473,7 +4314,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void automaticZenRuleToZenRule_allFields() { when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( new String[]{OWNER.getPackageName()}); @@ -4515,7 +4355,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_fromApp_updatesNameUnlessUserModified() { // Add a starting rule with the name OriginalName. AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID) @@ -4573,7 +4412,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_fromUser_updatesBitmaskAndValue() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) @@ -4627,7 +4465,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_fromSystemUi_updatesValues() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) @@ -4678,7 +4515,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_fromApp_updatesValuesIfRuleNotUserModified() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) @@ -4754,7 +4590,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_updatesValues() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) @@ -4777,11 +4612,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isTrue(); + assertThat(storedRule.isUserModified()).isFalse(); } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_nullDeviceEffectsUpdate() { // Adds a starting rule with empty zen policies and device effects ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); @@ -4809,7 +4643,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_nullPolicyUpdate() { // Adds a starting rule with set zen policy and empty device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) @@ -4838,7 +4671,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void automaticZenRuleToZenRule_nullToNonNullPolicyUpdate() { when(mContext.checkCallingPermission(anyString())) .thenReturn(PackageManager.PERMISSION_GRANTED); @@ -4888,7 +4720,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { STATE_DISALLOW); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_ALLOW_CHANNELS | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS @@ -4902,7 +4734,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void automaticZenRuleToZenRule_nullToNonNullDeviceEffectsUpdate() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) @@ -4931,7 +4762,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); - assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.isUserModified()).isTrue(); assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( ZenDeviceEffects.FIELD_GRAYSCALE); } @@ -5007,7 +4838,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testUpdateAutomaticRule_activated_triggersBroadcast() throws Exception { setupZenConfig(); @@ -5047,7 +4877,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testUpdateAutomaticRule_deactivatedByUser_triggersBroadcast() throws Exception { setupZenConfig(); @@ -5091,7 +4920,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testUpdateAutomaticRule_deactivatedByApp_triggersBroadcast() throws Exception { setupZenConfig(); @@ -5167,7 +4995,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_ruleChanged_deactivatesRule() { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID) @@ -5191,7 +5018,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_ruleNotChanged_doesNotDeactivateRule() { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID) @@ -5214,7 +5040,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule() { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID) @@ -5239,7 +5065,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void updateAutomaticZenRule_ruleChangedByUser_doesNotDeactivateRule_forWatch() { when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); @@ -5266,7 +5091,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_ruleDisabledByUser_doesNotReactivateOnReenable() { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); AutomaticZenRule rule = new AutomaticZenRule.Builder("rule", CONDITION_ID) @@ -5291,7 +5116,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_changeOwnerForSystemRule_allowed() { when(mContext.checkCallingPermission(anyString())) .thenReturn(PackageManager.PERMISSION_GRANTED); @@ -5314,7 +5139,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_changeOwnerForAppOwnedRule_ignored() { AutomaticZenRule original = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mContext.getPackageName(), "old.third.party.cps")) @@ -5335,7 +5160,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAutomaticZenRule_propagatesOriginToEffectsApplier() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); reset(mDeviceEffectsApplier); @@ -5358,7 +5182,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_applied() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); @@ -5384,7 +5207,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testSettingDeviceEffects_throwsExceptionIfAlreadySet() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); @@ -5394,7 +5216,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_onDeactivateRule_applied() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); @@ -5413,7 +5234,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_changeToConsolidatedEffects_applied() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); @@ -5453,7 +5273,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); @@ -5478,7 +5297,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_activeBeforeApplierProvided_appliedWhenProvided() { ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); String ruleId = addRuleWithEffects(zde); @@ -5494,7 +5312,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testDeviceEffects_onUserSwitch_appliedImmediately() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_INIT)); @@ -5522,7 +5339,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING}) + @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING}) public void testDeviceEffects_allowsGrayscale() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); reset(mDeviceEffectsApplier); @@ -5539,7 +5356,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING}) + @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING}) public void testDeviceEffects_whileDriving_avoidsGrayscale() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); reset(mDeviceEffectsApplier); @@ -5563,7 +5380,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING}) + @EnableFlags({FLAG_MODES_UI, FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING}) public void testDeviceEffects_whileDrivingWithGrayscale_allowsGrayscale() { mZenModeHelper.setDeviceEffectsApplier(mDeviceEffectsApplier); reset(mDeviceEffectsApplier); @@ -5594,7 +5411,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAndAddAutomaticZenRule_wasCustomized_isRestored() { // Start with a rule. mZenModeHelper.mConfig.automaticRules.clear(); @@ -5651,7 +5467,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAndAddAutomaticZenRule_wasNotCustomized_isNotRestored() { // Start with a single rule. mZenModeHelper.mConfig.automaticRules.clear(); @@ -5685,7 +5500,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAndAddAutomaticZenRule_recreatedButNotByApp_isNotRestored() { // Start with a single rule. mZenModeHelper.mConfig.automaticRules.clear(); @@ -5736,7 +5550,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAndAddAutomaticZenRule_removedByUser_isNotRestored() { // Start with a single rule. mZenModeHelper.mConfig.automaticRules.clear(); @@ -5779,7 +5592,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void removeAndAddAutomaticZenRule_ifChangingComponent_isAllowedAndDoesNotRestore() { // Start with a rule. mZenModeHelper.mConfig.automaticRules.clear(); @@ -5824,7 +5637,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() { mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS, PERMISSION_GRANTED); // So that canManageAZR passes although packages don't match. @@ -5874,7 +5686,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAllZenRules_preservedForRestoring() { mZenModeHelper.mConfig.automaticRules.clear(); @@ -5898,14 +5709,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAllZenRules_fromSystem_deletesPreservedRulesToo() { mZenModeHelper.mConfig.automaticRules.clear(); // Start with deleted rules from 2 different packages. Instant now = Instant.ofEpochMilli(1701796461000L); - ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now); - ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now); + ZenRule pkg1Rule = newDeletedZenRule("1", "pkg1", now.minus(1, DAYS), now); + ZenRule pkg2Rule = newDeletedZenRule("2", "pkg2", now.minus(2, DAYS), now); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule); @@ -5918,7 +5728,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAndAddAutomaticZenRule_wasActive_isRestoredAsInactive() { // Start with a rule. mZenModeHelper.mConfig.automaticRules.clear(); @@ -5968,7 +5777,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void removeAndAddAutomaticZenRule_wasSnoozed_isRestoredAsInactive() { // Start with a rule. mZenModeHelper.mConfig.automaticRules.clear(); @@ -6023,12 +5831,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testRuleCleanup() throws Exception { Instant now = Instant.ofEpochMilli(1701796461000L); - Instant yesterday = now.minus(1, ChronoUnit.DAYS); - Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS); - Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); mTestClock.setNowMillis(now.toEpochMilli()); when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt())) @@ -6041,24 +5848,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { config.user = 42; mZenModeHelper.mConfigs.put(42, config); // okay rules (not deleted, package exists, with a range of creation dates). - config.automaticRules.put("ar1", newZenRule("good_pkg", now, null)); - config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null)); - config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null)); + config.automaticRules.put("ar1", newZenRule("ar1", "good_pkg", now)); + config.automaticRules.put("ar2", newZenRule("ar2", "good_pkg", yesterday)); + config.automaticRules.put("ar3", newZenRule("ar3", "good_pkg", twoMonthsAgo)); // newish rules for a missing package - config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null)); + config.automaticRules.put("ar4", newZenRule("ar4", "bad_pkg", yesterday)); // oldish rules belonging to a missing package - config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null)); + config.automaticRules.put("ar5", newZenRule("ar5", "bad_pkg", aWeekAgo)); // rules deleted recently - config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday)); - config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo)); + config.deletedRules.put("del1", + newDeletedZenRule("del1", "good_pkg", twoMonthsAgo, yesterday)); + config.deletedRules.put("del2", + newDeletedZenRule("del2", "good_pkg", twoMonthsAgo, aWeekAgo)); // rules deleted a long time ago - config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo)); + config.deletedRules.put("del3", + newDeletedZenRule("del3", "good_pkg", twoMonthsAgo, twoMonthsAgo)); // rules for a missing package, created recently and deleted recently - config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now)); + config.deletedRules.put("del4", newDeletedZenRule("del4", "bad_pkg", yesterday, now)); // rules for a missing package, created a long time ago and deleted recently - config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now)); + config.deletedRules.put("del5", newDeletedZenRule("del5", "bad_pkg", twoMonthsAgo, now)); // rules for a missing package, created a long time ago and deleted a long time ago - config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo)); + config.deletedRules.put("del6", + newDeletedZenRule("del6", "bad_pkg", twoMonthsAgo, twoMonthsAgo)); mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. @@ -6068,20 +5879,120 @@ public class ZenModeHelperTest extends UiServiceTestCase { .containsExactly("del1", "del2", "del4"); } - private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) { + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_removesNotRecentlyUsedNotModifiedImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant yesterday = now.minus(1, DAYS); + Instant aWeekAgo = now.minus(7, DAYS); + Instant twoMonthsAgo = now.minus(60, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing a bunch of implicit rules + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // used recently + ZenRule usedRecently1 = newImplicitZenRule("pkg1", aYearAgo, yesterday); + ZenRule usedRecently2 = newImplicitZenRule("pkg2", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently1.id, usedRecently1); + config.automaticRules.put(usedRecently2.id, usedRecently2); + // not used in a long time + ZenRule longUnused = newImplicitZenRule("pkg3", aYearAgo, twoMonthsAgo); + config.automaticRules.put(longUnused.id, longUnused); + // created a long time ago, before lastActivation tracking + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", twoMonthsAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, before lastActivation tracking + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + // not used in a long time, but was customized by user + ZenRule longUnusedButCustomized = newImplicitZenRule("pkg6", aYearAgo, twoMonthsAgo); + longUnusedButCustomized.zenPolicyUserModifiedFields = ZenPolicy.FIELD_CONVERSATIONS; + config.automaticRules.put(longUnusedButCustomized.id, longUnusedButCustomized); + // created a long time ago, before lastActivation tracking, and was customized by user + ZenRule oldAndLastUsageUnknownAndCustomized = newImplicitZenRule("pkg7", twoMonthsAgo, + null); + oldAndLastUsageUnknownAndCustomized.userModifiedFields = AutomaticZenRule.FIELD_ICON; + config.automaticRules.put(oldAndLastUsageUnknownAndCustomized.id, + oldAndLastUsageUnknownAndCustomized); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // The recently used OR modified OR last-used-unknown rules stay. + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_METADATA) + .containsExactly(usedRecently1, usedRecently2, oldAndLastUsageUnknown, + newAndLastUsageUnknown, longUnusedButCustomized, + oldAndLastUsageUnknownAndCustomized); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void testRuleCleanup_assignsLastActivationToImplicitRules() throws Exception { + Instant now = Instant.ofEpochMilli(1701796461000L); + Instant aWeekAgo = now.minus(7, DAYS); + Instant aYearAgo = now.minus(365, DAYS); + mTestClock.setNowMillis(now.toEpochMilli()); + when(mPackageManager.getPackageInfo(anyString(), anyInt())).thenReturn(new PackageInfo()); + + // Set up a config to be loaded, containing implicit rules. + ZenModeConfig config = new ZenModeConfig(); + config.user = 42; + mZenModeHelper.mConfigs.put(42, config); + // with last activation known + ZenRule usedRecently = newImplicitZenRule("pkg1", aYearAgo, aWeekAgo); + config.automaticRules.put(usedRecently.id, usedRecently); + // created a long time ago, with last activation unknown + ZenRule oldAndLastUsageUnknown = newImplicitZenRule("pkg4", aYearAgo, null); + config.automaticRules.put(oldAndLastUsageUnknown.id, oldAndLastUsageUnknown); + // created a short time ago, with last activation unknown + ZenRule newAndLastUsageUnknown = newImplicitZenRule("pkg5", aWeekAgo, null); + config.automaticRules.put(newAndLastUsageUnknown.id, newAndLastUsageUnknown); + + mZenModeHelper.onUserSwitched(42); // copies config and cleans it up. + + // All rules stayed. + usedRecently = getZenRule(usedRecently.id); + oldAndLastUsageUnknown = getZenRule(oldAndLastUsageUnknown.id); + newAndLastUsageUnknown = getZenRule(newAndLastUsageUnknown.id); + + // The rules with an unknown last usage have been assigned a placeholder one. + assertThat(usedRecently.lastActivation).isEqualTo(aWeekAgo); + assertThat(oldAndLastUsageUnknown.lastActivation).isEqualTo(now); + assertThat(newAndLastUsageUnknown.lastActivation).isEqualTo(now); + } + + private static ZenRule newDeletedZenRule(String id, String pkg, Instant createdAt, + @NonNull Instant deletedAt) { + ZenRule rule = newZenRule(id, pkg, createdAt); + rule.deletionInstant = deletedAt; + return rule; + } + + private static ZenRule newImplicitZenRule(String pkg, @NonNull Instant createdAt, + @Nullable Instant lastActivatedAt) { + ZenRule implicitRule = newZenRule(implicitRuleId(pkg), pkg, createdAt); + implicitRule.lastActivation = lastActivatedAt; + return implicitRule; + } + + private static ZenRule newZenRule(String id, String pkg, Instant createdAt) { ZenRule rule = new ZenRule(); + rule.id = id; rule.pkg = pkg; rule.creationTime = createdAt.toEpochMilli(); rule.enabled = true; - rule.deletionInstant = deletedAt; + rule.deletionInstant = null; // Plus stuff so that isValidAutomaticRule() passes - rule.name = "A rule from " + pkg + " created on " + createdAt; + rule.name = "Rule " + id; rule.conditionId = Uri.parse(rule.name); return rule; } @Test - @EnableFlags(FLAG_MODES_API) public void getAutomaticZenRuleState_ownedRule_returnsRuleState() { String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), @@ -6112,14 +6023,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void getAutomaticZenRuleState_notOwnedRule_returnsStateUnknown() { // Assume existence of a system-owned rule that is currently ACTIVE. - ZenRule systemRule = newZenRule("android", Instant.now(), null); + ZenRule systemRule = newZenRule("systemRule", "android", Instant.now()); systemRule.zenMode = ZEN_MODE_ALARMS; systemRule.condition = new Condition(systemRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("systemRule", systemRule); + config.automaticRules.put(systemRule.id, systemRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -6128,15 +6038,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void setAutomaticZenRuleState_idForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -6149,15 +6058,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void setAutomaticZenRuleStateFromConditionProvider_conditionForNotOwnedRule_ignored() { // Assume existence of an other-package-owned rule that is currently ACTIVE. assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); - ZenRule otherRule = newZenRule("another.package", Instant.now(), null); + ZenRule otherRule = newZenRule("otherRule", "another.package", Instant.now()); otherRule.zenMode = ZEN_MODE_ALARMS; otherRule.condition = new Condition(otherRule.conditionId, "on", Condition.STATE_TRUE); ZenModeConfig config = mZenModeHelper.mConfig.copy(); - config.automaticRules.put("otherRule", otherRule); + config.automaticRules.put(otherRule.id, otherRule); mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); @@ -6171,7 +6079,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testCallbacks_policy() throws Exception { setupZenConfig(); assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).allowReminders()) @@ -6193,7 +6100,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void testCallbacks_consolidatedPolicy() throws Exception { assertThat(mZenModeHelper.getConsolidatedNotificationPolicy().allowMedia()).isTrue(); SettableFuture<Policy> futureConsolidatedPolicy = SettableFuture.create(); @@ -6219,7 +6125,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() { mZenModeHelper.mConfig.automaticRules.clear(); @@ -6235,7 +6140,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() { mZenModeHelper.mConfig.automaticRules.clear(); @@ -6257,7 +6161,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() { mZenModeHelper.mConfig.automaticRules.clear(); String pkg = mContext.getPackageName(); @@ -6290,7 +6193,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_ruleCustomizedButNotFilter_updatesRule() { mZenModeHelper.mConfig.automaticRules.clear(); String pkg = mContext.getPackageName(); @@ -6322,7 +6224,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() { mZenModeHelper.mConfig.automaticRules.clear(); mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, mPkg, CUSTOM_PKG_UID, @@ -6339,7 +6240,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() { mZenModeHelper.mConfig.automaticRules.clear(); @@ -6350,7 +6250,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() { mZenModeHelper.mConfig.automaticRules.clear(); @@ -6375,7 +6274,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void applyGlobalZenModeAsImplicitZenRule_again_refreshesRuleName() { mZenModeHelper.mConfig.automaticRules.clear(); mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, @@ -6394,7 +6293,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void applyGlobalZenModeAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() { mZenModeHelper.mConfig.automaticRules.clear(); mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, @@ -6420,19 +6319,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @DisableFlags(FLAG_MODES_API) - public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() { - mZenModeHelper.mConfig.automaticRules.clear(); - - withoutWtfCrash( - () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, - CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS)); - - assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); - } - - @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalPolicyAsImplicitZenRule_createsImplicitRule() { mZenModeHelper.mConfig.automaticRules.clear(); @@ -6457,7 +6343,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalPolicyAsImplicitZenRule_updatesImplicitRule() { mZenModeHelper.mConfig.automaticRules.clear(); @@ -6489,7 +6374,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalPolicyAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() { mZenModeHelper.mConfig.automaticRules.clear(); String pkg = mContext.getPackageName(); @@ -6532,7 +6416,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void applyGlobalPolicyAsImplicitZenRule_ruleCustomizedButNotZenPolicy_updatesRule() { mZenModeHelper.mConfig.automaticRules.clear(); String pkg = mContext.getPackageName(); @@ -6572,7 +6455,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void applyGlobalPolicyAsImplicitZenRule_again_refreshesRuleName() { mZenModeHelper.mConfig.automaticRules.clear(); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, @@ -6591,7 +6474,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void applyGlobalPolicyAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() { mZenModeHelper.mConfig.automaticRules.clear(); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, @@ -6617,19 +6500,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @DisableFlags(FLAG_MODES_API) - public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() { - mZenModeHelper.mConfig.automaticRules.clear(); - - withoutWtfCrash( - () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, - CUSTOM_PKG_NAME, CUSTOM_PKG_UID, new Policy(0, 0, 0))); - - assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); - } - - @Test - @EnableFlags(FLAG_MODES_API) public void getNotificationPolicyFromImplicitZenRule_returnsSetPolicy() { Policy writtenPolicy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, @@ -6645,7 +6515,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) @DisableFlags(FLAG_MODES_UI) public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() { // Implicit rule will get the global policy at the time of rule creation. @@ -6665,7 +6534,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() { Policy policy = new Policy(PRIORITY_CATEGORY_CALLS, PRIORITY_SENDERS_STARRED, 0); mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, policy, ORIGIN_APP, @@ -6680,7 +6548,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) @DisableFlags(FLAG_MODES_UI) public void setNotificationPolicy_updatesRulePolicies_ifRulePolicyIsDefaultOrGlobalPolicy() { ZenPolicy defaultZenPolicy = mZenModeHelper.getDefaultZenPolicy(); @@ -6726,7 +6593,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_API) public void addRule_iconIdWithResourceNameTooLong_ignoresIcon() { int resourceId = 999; String veryLongResourceName = "com.android.server.notification:drawable/" @@ -6745,7 +6611,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setManualZenRuleDeviceEffects_noPreexistingMode() { ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) @@ -6759,7 +6625,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setManualZenRuleDeviceEffects_preexistingMode() { mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, Uri.EMPTY, ORIGIN_USER_IN_SYSTEMUI, "create manual rule", "settings", SYSTEM_UID); @@ -6776,7 +6642,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void addAutomaticZenRule_startsDisabled_recordsDisabledOrigin() { AutomaticZenRule startsDisabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mPkg, "SomeProvider")) @@ -6792,7 +6658,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_disabling_recordsDisabledOrigin() { AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mPkg, "SomeProvider")) @@ -6815,7 +6681,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_keepingDisabled_preservesPreviousDisabledOrigin() { AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mPkg, "SomeProvider")) @@ -6845,7 +6711,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateAutomaticZenRule_enabling_clearsDisabledOrigin() { AutomaticZenRule startsEnabled = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mPkg, "SomeProvider")) @@ -6875,7 +6741,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_manualActivation_appliesOverride() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -6893,7 +6759,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_manualActivationAndThenDeactivation_removesOverride() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -6930,7 +6796,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_manualDeactivationAndThenReactivation_removesOverride() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -6976,7 +6842,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_manualDeactivation_appliesOverride() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -7002,7 +6868,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_ifManualActive_appCannotDeactivateBeforeActivating() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -7039,7 +6905,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_ifManualInactive_appCannotReactivateBeforeDeactivating() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -7085,7 +6951,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_withActivationOverride_userActionFromAppCanDeactivate() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -7109,7 +6975,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_withDeactivationOverride_userActionFromAppCanActivate() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -7140,7 +7006,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_manualActionFromApp_isNotOverride() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) @@ -7165,7 +7031,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_implicitRuleManualActivation_doesNotUseOverride() { mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS, PERMISSION_GRANTED); // So that canManageAZR succeeds. @@ -7188,7 +7054,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_implicitRuleManualDeactivation_doesNotUseOverride() { mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS, PERMISSION_GRANTED); // So that canManageAZR succeeds. @@ -7214,24 +7080,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @DisableFlags({FLAG_MODES_API, FLAG_MODES_UI}) - public void testDefaultConfig_preModesApi_rulesAreBare() { - // Create a new user, which should get a copy of the default policy. - mZenModeHelper.onUserSwitched(101); - - ZenRule eventsRule = mZenModeHelper.mConfig.automaticRules.get( - ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); - - assertThat(eventsRule).isNotNull(); - assertThat(eventsRule.zenPolicy).isNull(); - assertThat(eventsRule.type).isEqualTo(TYPE_UNKNOWN); - assertThat(eventsRule.triggerDescription).isNull(); - } - - @Test - @EnableFlags(FLAG_MODES_API) @DisableFlags(FLAG_MODES_UI) - public void testDefaultConfig_modesApi_rulesHaveFullPolicy() { + public void testDefaultConfig_rulesHaveFullPolicy() { // Create a new user, which should get a copy of the default policy. mZenModeHelper.onUserSwitched(201); @@ -7245,7 +7095,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void testDefaultConfig_modesUi_rulesHaveFullPolicy() { // Create a new user, which should get a copy of the default policy. mZenModeHelper.onUserSwitched(301); @@ -7260,7 +7110,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_withManualActivation_activeOnReboot() throws Exception { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) @@ -7298,7 +7148,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void setAutomaticZenRuleState_withManualDeactivation_clearedOnReboot() throws Exception { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) @@ -7336,7 +7186,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_MODES_API) public void addAutomaticZenRule_withoutPolicy_getsItsOwnInstanceOfDefaultPolicy() { // Add a rule without policy -> uses default config AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) @@ -7352,7 +7201,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void readXml_withDisabledEventsRule_deletesIt() throws Exception { ZenRule rule = new ZenRule(); rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID; @@ -7372,7 +7221,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void readXml_withEnabledEventsRule_keepsIt() throws Exception { ZenRule rule = new ZenRule(); rule.id = ZenModeConfig.EVENTS_OBSOLETE_RULE_ID; @@ -7392,7 +7241,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + @EnableFlags(FLAG_MODES_UI) public void updateHasPriorityChannels_keepsChannelSettings() { setupZenConfig(); @@ -7512,6 +7361,125 @@ public class ZenModeHelperTest extends UiServiceTestCase { "config: setAzrStateFromCps: cond/cond (ORIGIN_APP) from uid " + CUSTOM_PKG_UID); } + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_updatesLastActivation() { + String ruleOne = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String ruleTwo = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("unrelated", Uri.parse("other.condition")) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isNull(); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_FALSE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(firstActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + + Instant secondActivation = Instant.ofEpochMilli(500); + mTestClock.setNow(secondActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleOne, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleOne).lastActivation).isEqualTo(secondActivation); + assertThat(getZenRule(ruleTwo).lastActivation).isNull(); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setManualZenMode_updatesLastActivation() { + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isNull(); + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_ALARMS, null, + ORIGIN_USER_IN_SYSTEMUI, "reason", "systemui", SYSTEM_UID); + + assertThat(mZenModeHelper.mConfig.manualRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void applyGlobalZenModeAsImplicitZenRule_updatesLastActivation() { + Instant instant = Instant.ofEpochMilli(100); + mTestClock.setNow(instant); + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_ALARMS); + + ZenRule implicitRule = getZenRule(implicitRuleId(CUSTOM_PKG_NAME)); + assertThat(implicitRule.lastActivation).isEqualTo(instant); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void setAutomaticZenRuleState_notChangingActiveState_doesNotUpdateLastActivation() { + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(), + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + // Manual activation comes first + Instant firstActivation = Instant.ofEpochMilli(100); + mTestClock.setNow(firstActivation); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + + // Now the app says the rule should be active (assume it's on a schedule, and the app + // doesn't listen to broadcasts so it doesn't know an override was present). This doesn't + // change the activation state. + mTestClock.setNow(Instant.ofEpochMilli(300)); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isEqualTo(firstActivation); + } + + @Test + @EnableFlags({FLAG_MODES_UI, FLAG_MODES_CLEANUP_IMPLICIT}) + public void addOrUpdateRule_doesNotUpdateLastActivation() { + AutomaticZenRule azr = new AutomaticZenRule.Builder("rule", CONDITION_ID) + .setConfigurationActivity(new ComponentName(mPkg, "cls")) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .build(); + + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, azr, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, + new AutomaticZenRule.Builder(azr).setName("New name").build(), ORIGIN_APP, "reason", + CUSTOM_PKG_UID); + + assertThat(getZenRule(ruleId).lastActivation).isNull(); + } + private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @Nullable ZenPolicy zenPolicy) { ZenRule rule = new ZenRule(); @@ -7529,22 +7497,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { } private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = - Correspondence.transforming(zr -> { - Parcel p = Parcel.obtain(); - try { - zr.writeToParcel(p, 0); - p.setDataPosition(0); - ZenRule copy = new ZenRule(p); - copy.creationTime = 0; - copy.userModifiedFields = 0; - copy.zenPolicyUserModifiedFields = 0; - copy.zenDeviceEffectsUserModifiedFields = 0; - return copy; - } finally { - p.recycle(); - } - }, - "Ignoring timestamp and userModifiedFields"); + Correspondence.transforming( + ZenModeHelperTest::cloneWithoutMetadata, + ZenModeHelperTest::cloneWithoutMetadata, + "Ignoring timestamps and userModifiedFields"); + + private static ZenRule cloneWithoutMetadata(ZenRule rule) { + Parcel p = Parcel.obtain(); + try { + rule.writeToParcel(p, 0); + p.setDataPosition(0); + ZenRule copy = new ZenRule(p); + copy.creationTime = 0; + copy.userModifiedFields = 0; + copy.zenPolicyUserModifiedFields = 0; + copy.zenDeviceEffectsUserModifiedFields = 0; + copy.lastActivation = null; + return copy; + } finally { + p.recycle(); + } + } private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { @@ -7574,7 +7547,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { return rule; } - // TODO: b/310620812 - Update setup methods to include allowChannels() when MODES_API is inlined private void setupZenConfig() { Policy customPolicy = new Policy(PRIORITY_CATEGORY_REMINDERS | PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_MESSAGES @@ -7583,7 +7555,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, SUPPRESSED_EFFECT_BADGE, - 0, + 0, // allows priority channels. CONVERSATION_SENDERS_IMPORTANT); mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, customPolicy, ORIGIN_UNKNOWN, 1); if (!Flags.modesUi()) { @@ -7599,8 +7571,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(STATE_ALLOW, dndProto.getCalls().getNumber()); assertEquals(PEOPLE_STARRED, dndProto.getAllowCallsFrom().getNumber()); assertEquals(STATE_ALLOW, dndProto.getMessages().getNumber()); + assertEquals(PEOPLE_STARRED, dndProto.getAllowMessagesFrom().getNumber()); assertEquals(STATE_ALLOW, dndProto.getEvents().getNumber()); assertEquals(STATE_ALLOW, dndProto.getRepeatCallers().getNumber()); + assertEquals(STATE_ALLOW, dndProto.getConversations().getNumber()); + assertEquals(CONV_IMPORTANT, dndProto.getAllowConversationsFrom().getNumber()); assertEquals(STATE_ALLOW, dndProto.getFullscreen().getNumber()); assertEquals(STATE_ALLOW, dndProto.getLights().getNumber()); assertEquals(STATE_ALLOW, dndProto.getPeek().getNumber()); @@ -7616,7 +7591,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { return; } - // When modes_api flag is on, the default zen config is the device defaults. + // The default zen config is the device defaults. assertThat(dndProto.getAlarms().getNumber()).isEqualTo(STATE_ALLOW); assertThat(dndProto.getMedia().getNumber()).isEqualTo(STATE_ALLOW); assertThat(dndProto.getSystem().getNumber()).isEqualTo(STATE_DISALLOW); @@ -7627,6 +7602,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(dndProto.getAllowMessagesFrom().getNumber()).isEqualTo(PEOPLE_STARRED); assertThat(dndProto.getEvents().getNumber()).isEqualTo(STATE_DISALLOW); assertThat(dndProto.getRepeatCallers().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getConversations().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAllowConversationsFrom().getNumber()).isEqualTo(CONV_IMPORTANT); assertThat(dndProto.getFullscreen().getNumber()).isEqualTo(STATE_DISALLOW); assertThat(dndProto.getLights().getNumber()).isEqualTo(STATE_DISALLOW); assertThat(dndProto.getPeek().getNumber()).isEqualTo(STATE_DISALLOW); @@ -7634,6 +7611,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(dndProto.getBadge().getNumber()).isEqualTo(STATE_ALLOW); assertThat(dndProto.getAmbient().getNumber()).isEqualTo(STATE_DISALLOW); assertThat(dndProto.getNotificationList().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAllowChannels().getNumber()).isEqualTo( + DNDProtoEnums.CHANNEL_POLICY_PRIORITY); } private static String getZenLog() { @@ -7642,16 +7621,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { return zenLogWriter.toString(); } - private static void withoutWtfCrash(Runnable test) { - Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> { - }); - try { - test.run(); - } finally { - Log.setWtfHandler(oldHandler); - } - } - /** * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml() */ @@ -7954,6 +7923,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { return mNowMillis; } + private void setNow(Instant instant) { + mNowMillis = instant.toEpochMilli(); + } + private void setNowMillis(long millis) { mNowMillis = millis; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java index 6433b76defc3..8b9376454c0f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; -import android.app.Flags; import android.os.Parcel; import android.platform.test.flag.junit.SetFlagsRule; import android.service.notification.ZenPolicy; @@ -34,7 +33,6 @@ import com.android.server.UiServiceTestCase; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,11 +48,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Before - public final void setUp() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - } - @Test public void testZenPolicyApplyAllowedToDisallowed() { ZenPolicy.Builder builder = new ZenPolicy.Builder(); @@ -207,8 +200,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Test public void testZenPolicyApplyChannels_applyUnset() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy unset = builder.build(); @@ -223,8 +214,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Test public void testZenPolicyApplyChannels_applyStricter() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - ZenPolicy.Builder builder = new ZenPolicy.Builder(); builder.allowPriorityChannels(false); ZenPolicy none = builder.build(); @@ -239,8 +228,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Test public void testZenPolicyApplyChannels_applyLooser() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - ZenPolicy.Builder builder = new ZenPolicy.Builder(); builder.allowPriorityChannels(false); ZenPolicy none = builder.build(); @@ -255,8 +242,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Test public void testZenPolicyApplyChannels_applySet() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy unset = builder.build(); @@ -270,8 +255,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Test public void testZenPolicyOverwrite_allUnsetPolicies() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy unset = builder.build(); @@ -292,8 +275,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Test public void testZenPolicyOverwrite_someOverlappingFields_takeNewPolicy() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - ZenPolicy p1 = new ZenPolicy.Builder() .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED) @@ -375,7 +356,6 @@ public class ZenPolicyTest extends UiServiceTestCase { @Test public void testEmptyZenPolicy_emptyChannels() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy policy = builder.build(); @@ -688,22 +668,7 @@ public class ZenPolicyTest extends UiServiceTestCase { } @Test - public void testAllowChannels_noFlag() { - mSetFlagsRule.disableFlags(Flags.FLAG_MODES_API); - - // allowChannels should be unset, not be modifiable, and not show up in any output - ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.allowPriorityChannels(true); - ZenPolicy policy = builder.build(); - - assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); - assertThat(policy.toString().contains("allowChannels")).isFalse(); - } - - @Test public void testAllowChannels() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); - // allow priority channels ZenPolicy.Builder builder = new ZenPolicy.Builder(); builder.allowPriorityChannels(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index 3c74ad06a21f..a9be47d71213 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -509,6 +509,32 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue(); } + @Test + public void testOpaque_nonLeafTaskFragmentWithDirectActivity_opaque() { + final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true) + .build(); + directChildActivity.setOccludesParent(true); + final Task nonLeafTask = directChildActivity.getTask(); + final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(), + true /* createdByOrganizer */, false /* isEmbedded */); + nonLeafTask.addChild(directChildFragment, 0); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isTrue(); + } + + @Test + public void testOpaque_nonLeafTaskFragmentWithDirectActivity_transparent() { + final ActivityRecord directChildActivity = new ActivityBuilder(mAtm).setCreateTask(true) + .build(); + directChildActivity.setOccludesParent(false); + final Task nonLeafTask = directChildActivity.getTask(); + final TaskFragment directChildFragment = new TaskFragment(mAtm, new Binder(), + true /* createdByOrganizer */, false /* isEmbedded */); + nonLeafTask.addChild(directChildFragment, 0); + + assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(nonLeafTask)).isFalse(); + } + @NonNull private TaskFragment createChildTaskFragment(@NonNull Task parent, @WindowConfiguration.WindowingMode int windowingMode, diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java index eaffc481098e..1e91bedb5c18 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -16,8 +16,6 @@ package com.android.server.wm; -import static android.provider.Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES; - import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.doReturn; @@ -30,7 +28,6 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; -import android.provider.Settings; import android.window.DesktopModeFlags; import androidx.test.filters.SmallTest; @@ -74,14 +71,12 @@ public class DesktopModeHelperTest { doReturn(mContext.getContentResolver()).when(mMockContext).getContentResolver(); resetDesktopModeFlagsCache(); resetEnforceDeviceRestriction(); - resetFlagOverride(); } @After public void tearDown() throws Exception { resetDesktopModeFlagsCache(); resetEnforceDeviceRestriction(); - resetFlagOverride(); } @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, @@ -167,7 +162,8 @@ public class DesktopModeHelperTest { @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @Test - public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() { + public void canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() + throws Exception { doReturn(true).when(mMockResources).getBoolean( eq(R.bool.config_isDesktopModeDevOptionSupported) ); @@ -177,22 +173,41 @@ public class DesktopModeHelperTest { } @Test - public void isDeviceEligibleForDesktopMode_configDEModeOn_returnsTrue() { + public void isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktop_returnsTrue() { + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(true).when(mMockResources) + .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); + + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); + } + + @Test + public void isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() { + doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); + doReturn(false).when(mMockResources) + .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); + + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); + } + + @Test + public void isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() { + doReturn(false).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)); doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)); - assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); } @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOff_returnsFalse() { - assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @Test public void isDeviceEligibleForDesktopMode_supportFlagOn_returnsFalse() { - assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isFalse(); + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isFalse(); } @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) @@ -202,7 +217,7 @@ public class DesktopModeHelperTest { eq(R.bool.config_isDesktopModeDevOptionSupported) ); - assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); } private void resetEnforceDeviceRestriction() throws Exception { @@ -227,13 +242,10 @@ public class DesktopModeHelperTest { cachedToggleOverride.set(/* obj= */ null, /* value= */ null); } - private void resetFlagOverride() { - Settings.Global.putString(mContext.getContentResolver(), - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, null); - } - - private void setFlagOverride(DesktopModeFlags.ToggleOverride override) { - Settings.Global.putInt(mContext.getContentResolver(), - DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.getSetting()); + private void setFlagOverride(DesktopModeFlags.ToggleOverride override) throws Exception { + Field cachedToggleOverride = DesktopModeFlags.class.getDeclaredField( + "sCachedToggleOverride"); + cachedToggleOverride.setAccessible(true); + cachedToggleOverride.set(/* obj= */ null, /* value= */ override); } }
\ No newline at end of file diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 82435b24dad6..6e109a8d4eaf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -164,6 +164,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.BooleanSupplier; /** * Tests for the {@link DisplayContent} class. @@ -2674,16 +2675,67 @@ public class DisplayContentTests extends WindowTestsBase { public void testKeyguardGoingAwayWhileAodShown() { mDisplayContent.getDisplayPolicy().setAwake(true); - final WindowState appWin = newWindowBuilder("appWin", TYPE_APPLICATION).setDisplay( - mDisplayContent).build(); - final ActivityRecord activity = appWin.mActivityRecord; + final KeyguardController keyguard = mAtm.mKeyguardController; + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final int displayId = mDisplayContent.getDisplayId(); + + final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); + final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); + final BooleanSupplier appVisible = activity::isVisibleRequested; + + // Begin locked and in AOD + keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); + + // Start unlocking from AOD. + keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); - mAtm.mKeyguardController.setKeyguardShown(appWin.getDisplayId(), true /* keyguardShowing */, - true /* aodShowing */); - assertFalse(activity.isVisibleRequested()); + // Clear AOD. This does *not* clear the going-away status. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Finish unlock + keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + } + + @Test + public void testKeyguardGoingAwayCanceledWhileAodShown() { + mDisplayContent.getDisplayPolicy().setAwake(true); - mAtm.mKeyguardController.keyguardGoingAway(appWin.getDisplayId(), 0 /* flags */); - assertTrue(activity.isVisibleRequested()); + final KeyguardController keyguard = mAtm.mKeyguardController; + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final int displayId = mDisplayContent.getDisplayId(); + + final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId); + final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId); + final BooleanSupplier appVisible = activity::isVisibleRequested; + + // Begin locked and in AOD + keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); + + // Start unlocking from AOD. + keyguard.keyguardGoingAway(displayId, 0x0 /* flags */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Clear AOD. This does *not* clear the going-away status. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardGoingAway.getAsBoolean()); + assertTrue(appVisible.getAsBoolean()); + + // Same API call a second time cancels the unlock, because AOD isn't changing. + keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */); + assertTrue(keyguardShowing.getAsBoolean()); + assertFalse(keyguardGoingAway.getAsBoolean()); + assertFalse(appVisible.getAsBoolean()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 95bca2b17efb..1dc32b00acba 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4486,6 +4486,49 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @DisableCompatChanges({ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED}) + public void testInFreeform_boundsSandboxedToAppBounds() { + allowDesktopMode(); + final int dw = 2800; + final int dh = 1400; + final int notchHeight = 100; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) + .setNotch(notchHeight) + .build(); + setUpApp(display); + prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); + + mTask.mDisplayContent.getDefaultTaskDisplayArea() + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); + mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); + Rect appBounds = new Rect(0, 0, 1000, 500); + Rect bounds = new Rect(0, 0, 1000, 600); + mTask.getWindowConfiguration().setAppBounds(appBounds); + mTask.getWindowConfiguration().setBounds(bounds); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + + // Bounds are sandboxed to appBounds in freeform. + assertDownScaled(); + assertEquals(mActivity.getWindowConfiguration().getAppBounds(), + mActivity.getWindowConfiguration().getBounds()); + + // Exit freeform. + mTask.mDisplayContent.getDefaultTaskDisplayArea() + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); + mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + mTask.getWindowConfiguration().setBounds(new Rect(0, 0, dw, dh)); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + assertFitted(); + appBounds = mActivity.getWindowConfiguration().getAppBounds(); + bounds = mActivity.getWindowConfiguration().getBounds(); + // Bounds are not sandboxed to appBounds. + assertNotEquals(appBounds, bounds); + assertEquals(notchHeight, appBounds.top - bounds.top); + } + + + @Test @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { final TaskBuilder taskBuilder = diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java index 12b744546f5e..9367941e32a3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java @@ -18,12 +18,16 @@ package com.android.server.wm; import static android.tools.traces.Utils.busyWaitForDataSourceRegistration; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -34,11 +38,15 @@ import android.platform.test.annotations.Presubmit; import android.tools.ScenarioBuilder; import android.tools.traces.io.ResultWriter; import android.tools.traces.monitors.PerfettoTraceMonitor; +import android.util.Log; import android.view.Choreographer; +import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; +import androidx.test.uiautomator.UiDevice; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -51,14 +59,15 @@ import java.io.IOException; /** * Test class for {@link WindowTracingPerfetto}. */ +@FlakyTest(bugId = 372558379) @SmallTest @Presubmit public class WindowTracingPerfettoTest { private static final String TEST_DATA_SOURCE_NAME = "android.windowmanager.test"; private static WindowManagerService sWmMock; - private static Choreographer sChoreographer; private static WindowTracing sWindowTracing; + private static Boolean sIsDataSourceRegisteredSuccessfully; private PerfettoTraceMonitor mTraceMonitor; @@ -66,19 +75,39 @@ public class WindowTracingPerfettoTest { public static void setUpOnce() throws Exception { sWmMock = Mockito.mock(WindowManagerService.class); Mockito.doNothing().when(sWmMock).dumpDebugLocked(Mockito.any(), Mockito.anyInt()); - sChoreographer = Mockito.mock(Choreographer.class); - sWindowTracing = new WindowTracingPerfetto(sWmMock, sChoreographer, + sWindowTracing = new WindowTracingPerfetto(sWmMock, Mockito.mock(Choreographer.class), new WindowManagerGlobalLock(), TEST_DATA_SOURCE_NAME); - busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME); + } + + @AfterClass + public static void tearDownOnce() { + sWmMock = null; + sWindowTracing = null; } @Before public void setUp() throws IOException { - Mockito.clearInvocations(sWmMock); + if (sIsDataSourceRegisteredSuccessfully != null) { + assumeTrue("Failed to register data source", sIsDataSourceRegisteredSuccessfully); + return; + } + try { + busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME); + sIsDataSourceRegisteredSuccessfully = true; + } catch (Exception e) { + sIsDataSourceRegisteredSuccessfully = false; + final String perfettoStatus = UiDevice.getInstance(getInstrumentation()) + .executeShellCommand("perfetto --query"); + Log.e(WindowTracingPerfettoTest.class.getSimpleName(), + "Failed to register data source: " + perfettoStatus); + // Only fail once. The rest tests will be skipped by assumeTrue. + fail("Failed to register data source"); + } } @After public void tearDown() throws IOException { + Mockito.clearInvocations(sWmMock); stopTracing(); } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 0b3d720bf52a..1a932859b750 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -10207,6 +10207,17 @@ public class CarrierConfigManager { "carrier_supported_satellite_notification_hysteresis_sec_int"; /** + * Satellite notification display restriction reset time in seconds. + * + * The device shows a notification when it connects to a satellite. If the user interacts + * with the notification, it won't be shown again immediately. Instead, the notification + * will only reappear after below key mentioned amount of time has passed. + */ + @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS) + public static final String KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT = + "satellite_connected_notification_throttle_millis_int"; + + /** * An integer key holds the timeout duration in seconds used to determine whether to exit * carrier-roaming NB-IOT satellite mode. * @@ -11428,6 +11439,10 @@ public class CarrierConfigManager { 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); + if (Flags.starlinkDataBugfix()) { + sDefaults.putLong(KEY_SATELLITE_CONNECTED_NOTIFICATION_THROTTLE_MILLIS_INT, + TimeUnit.DAYS.toMillis(7)); + } sDefaults.putInt(KEY_SATELLITE_ROAMING_SCREEN_OFF_INACTIVITY_TIMEOUT_SEC_INT, 30); sDefaults.putInt(KEY_SATELLITE_ROAMING_P2P_SMS_INACTIVITY_TIMEOUT_SEC_INT, 180); sDefaults.putInt(KEY_SATELLITE_ROAMING_ESOS_INACTIVITY_TIMEOUT_SEC_INT, 600); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index b7b209b78300..100690dcbb65 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -821,6 +821,25 @@ public final class SatelliteManager { "android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT"; /** + * A boolean value indicating whether application is optimized to utilize low bandwidth + * satellite data. + * The applications that are optimized for low bandwidth satellite data should set this + * property to {@code true} in the manifest to indicate to platform about the same. + * {@code + * <application> + * <meta-data + * android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED" + * android:value="true"/> + * </application> + * } + * <p> + * When {@code true}, satellite data optimized network is available for applications. + */ + @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS) + public static final String PROPERTY_SATELLITE_DATA_OPTIMIZED = + "android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"; + + /** * Registers a {@link SatelliteStateChangeListener} to receive callbacks when the satellite * state may have changed. * @@ -3840,6 +3859,35 @@ public final class SatelliteManager { } } + /** + * Get list of application packages name that are optimized for low bandwidth satellite data. + * + * @return List of application packages name with data optimized network property. + * + * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED} + * + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) + @FlaggedApi(Flags.FLAG_SATELLITE_25Q4_APIS) + public @NonNull List<String> getSatelliteDataOptimizedApps() { + List<String> appsNames = new ArrayList<>(); + try { + ITelephony telephony = getITelephony(); + if (telephony != null) { + appsNames = telephony.getSatelliteDataOptimizedApps(); + } else { + throw new IllegalStateException("telephony service is null."); + } + } catch (RemoteException ex) { + loge("getSatelliteDataOptimizedApps() RemoteException:" + ex); + ex.rethrowAsRuntimeException(); + } + + return appsNames; + } + @Nullable private static ITelephony getITelephony() { ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 08c003027c5b..1c6652daf498 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3596,4 +3596,15 @@ interface ITelephony { * @hide */ int getCarrierIdFromIdentifier(in CarrierIdentifier carrierIdentifier); + + + /** + * Get list of applications that are optimized for low bandwidth satellite data. + * + * @return List of Application Name with data optimized network property. + * {@link #PROPERTY_SATELLITE_DATA_OPTIMIZED} + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.SATELLITE_COMMUNICATION)") + List<String> getSatelliteDataOptimizedApps(); } diff --git a/tests/AppJankTest/res/values/strings.xml b/tests/AppJankTest/res/values/strings.xml new file mode 100644 index 000000000000..ab2d18fa9d53 --- /dev/null +++ b/tests/AppJankTest/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="continue_test">Continue Test</string> +</resources>
\ No newline at end of file diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java index fe9f63615757..3498974b348e 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java +++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java @@ -209,7 +209,8 @@ public class IntegrationTests { JankTrackerActivity.class); resumeJankTracker.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); mEmptyActivity.startActivity(resumeJankTracker); - mDevice.wait(Until.findObject(By.text("Edit Text")), WAIT_FOR_TIMEOUT_MS); + mDevice.wait(Until.findObject(By.text(mEmptyActivity.getString(R.string.continue_test))), + WAIT_FOR_TIMEOUT_MS); assertTrue(jankTracker.shouldTrack()); } diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java index 80ab6ad3e587..686758200853 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java @@ -18,15 +18,41 @@ package android.app.jank.tests; import android.app.Activity; import android.os.Bundle; +import android.widget.EditText; public class JankTrackerActivity extends Activity { + private static final int CONTINUE_TEST_DELAY_MS = 4000; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.jank_tracker_activity_layout); } + + /** + * In IntegrationTests#jankTrackingResumed_whenActivityBecomesVisibleAgain this activity is + * placed into the background and then resumed via an intent. The test waits until the + * `continue_test` string is visible on the screen before validating that Jank tracking has + * resumed. + * + * <p>The 4 second delay allows JankTracker to re-register its callbacks and start receiving + * JankData before the test proceeds. + */ + @Override + protected void onResume() { + super.onResume(); + getActivityThread().getHandler().postDelayed(new Runnable() { + @Override + public void run() { + EditText editTextView = findViewById(R.id.edit_text); + if (editTextView != null) { + editTextView.setText(R.string.continue_test); + } + } + }, CONTINUE_TEST_DELAY_MS); + } } diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java index c38517ace5e6..586bb76388f6 100644 --- a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java +++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java @@ -277,6 +277,72 @@ public class CertificateRevocationStatusManagerTest { } } + @Test + public void checkRevocationStatus_allCertificatesRecentlyChecked_doesNotFetchRemoteCrl() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // indirectly verifies the remote list is not fetched by simulating a remote revocation + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + + // no exception + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_allCertificatesBarelyRecentlyChecked_doesNotFetchRemoteCrl() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + Map<String, LocalDateTime> lastCheckedDates = new HashMap<>(); + LocalDateTime barelyRecently = + LocalDateTime.now() + .minusHours( + CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK - 1); + for (X509Certificate certificate : mCertificates1) { + lastCheckedDates.put(getSerialNumber(certificate), barelyRecently); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates); + + // Indirectly verify the remote CRL is not checked by checking there is no exception despite + // a certificate being revoked. This test differs from the next only in the lastCheckedDate, + // one before the NUM_HOURS_BEFORE_NEXT_CHECK cutoff and one after + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_certificatesRevokedAfterCheck_throwsException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + Map<String, LocalDateTime> lastCheckedDates = new HashMap<>(); + // To save network use, we do not check the remote CRL if all the certificates are recently + // checked, so we set the lastCheckDate to some time not recent. + LocalDateTime notRecently = + LocalDateTime.now() + .minusHours( + CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK + 1); + for (X509Certificate certificate : mCertificates1) { + lastCheckedDates.put(getSerialNumber(certificate), notRecently); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + private List<X509Certificate> getCertificateChain(String fileName) throws Exception { Collection<? extends Certificate> certificates = mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName)); diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 6caf3f973618..a2f6f0051116 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -333,7 +333,7 @@ class InputManagerServiceTests { fun testKeyActivenessNotifyEventsLifecycle() { service.systemRunning() - fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY); + fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY) val inputManager = context.getSystemService(InputManager::class.java) diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index 83d22d923c78..4d379e45a81a 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -18,18 +18,24 @@ package android.os.test; import static org.junit.Assert.assertTrue; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; +import android.os.TestLooperManager; import android.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Queue; import java.util.concurrent.Executor; /** @@ -44,7 +50,9 @@ import java.util.concurrent.Executor; * The Robolectric class also allows advancing time. */ public class TestLooper { - protected final Looper mLooper; + private final Looper mLooper; + private final TestLooperManager mTestLooperManager; + private final Clock mClock; private static final Constructor<Looper> LOOPER_CONSTRUCTOR; private static final Field THREAD_LOCAL_LOOPER_FIELD; @@ -54,24 +62,46 @@ public class TestLooper { private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; - private final Clock mClock; - private AutoDispatchThread mAutoDispatchThread; + /** + * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. + */ + private static boolean isAtLeastBaklava() { + Method[] methods = TestLooperManager.class.getMethods(); + for (Method method : methods) { + if (method.getName().equals("peekWhen")) { + return true; + } + } + return false; + // TODO(shayba): delete the above, uncomment the below. + // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. + // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + } + static { try { LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE); LOOPER_CONSTRUCTOR.setAccessible(true); THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal"); THREAD_LOCAL_LOOPER_FIELD.setAccessible(true); - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); - MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + + if (isAtLeastBaklava()) { + MESSAGE_QUEUE_MESSAGES_FIELD = null; + MESSAGE_NEXT_FIELD = null; + MESSAGE_WHEN_FIELD = null; + MESSAGE_MARK_IN_USE_METHOD = null; + } else { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); + MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + } } catch (NoSuchFieldException | NoSuchMethodException e) { throw new RuntimeException("Failed to initialize TestLooper", e); } @@ -106,6 +136,13 @@ public class TestLooper { throw new RuntimeException("Reflection error constructing or accessing looper", e); } + if (isAtLeastBaklava()) { + mTestLooperManager = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper); + } else { + mTestLooperManager = null; + } + mClock = clock; } @@ -117,19 +154,61 @@ public class TestLooper { return new HandlerExecutor(new Handler(getLooper())); } - private Message getMessageLinkedList() { + private Message getMessageLinkedListLegacy() { try { MessageQueue queue = mLooper.getQueue(); return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); } catch (IllegalAccessException e) { throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages", - e); + e); } } public void moveTimeForward(long milliSeconds) { + if (isAtLeastBaklava()) { + moveTimeForwardBaklava(milliSeconds); + } else { + moveTimeForwardLegacy(milliSeconds); + } + } + + private void moveTimeForwardBaklava(long milliSeconds) { + // Drain all Messages from the queue. + Queue<Message> messages = new ArrayDeque<>(); + while (true) { + Message message = mTestLooperManager.poll(); + if (message == null) { + break; + } + messages.add(message); + } + + // Repost all Messages back to the queue with a new time. + while (true) { + Message message = messages.poll(); + if (message == null) { + break; + } + + // Ugly trick to reset the Message's "in use" flag. + // This is needed because the Message cannot be re-enqueued if it's + // marked in use. + message.copyFrom(message); + + // Adjust the Message's delivery time. + long newWhen = message.getWhen() - milliSeconds; + if (newWhen < 0) { + newWhen = 0; + } + + // Send the Message back to its Handler to be re-enqueued. + message.getTarget().sendMessageAtTime(message, newWhen); + } + } + + private void moveTimeForwardLegacy(long milliSeconds) { try { - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -147,12 +226,12 @@ public class TestLooper { return mClock.uptimeMillis(); } - private Message messageQueueNext() { + private Message messageQueueNextLegacy() { try { long now = currentTime(); Message prevMsg = null; - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); if (msg != null && msg.getTarget() == null) { // Stalled by a barrier. Find the next asynchronous message in // the queue. @@ -185,18 +264,46 @@ public class TestLooper { /** * @return true if there are pending messages in the message queue */ - public synchronized boolean isIdle() { - Message messageList = getMessageLinkedList(); + public boolean isIdle() { + if (isAtLeastBaklava()) { + return isIdleBaklava(); + } else { + return isIdleLegacy(); + } + } + + private boolean isIdleBaklava() { + Long when = mTestLooperManager.peekWhen(); + return when != null && currentTime() >= when; + } + private synchronized boolean isIdleLegacy() { + Message messageList = getMessageLinkedListLegacy(); return messageList != null && currentTime() >= messageList.getWhen(); } /** * @return the next message in the Looper's message queue or null if there is none */ - public synchronized Message nextMessage() { + public Message nextMessage() { + if (isAtLeastBaklava()) { + return nextMessageBaklava(); + } else { + return nextMessageLegacy(); + } + } + + private Message nextMessageBaklava() { + if (isIdle()) { + return mTestLooperManager.poll(); + } else { + return null; + } + } + + private synchronized Message nextMessageLegacy() { if (isIdle()) { - return messageQueueNext(); + return messageQueueNextLegacy(); } else { return null; } @@ -206,9 +313,26 @@ public class TestLooper { * Dispatch the next message in the queue * Asserts that there is a message in the queue */ - public synchronized void dispatchNext() { + public void dispatchNext() { + if (isAtLeastBaklava()) { + dispatchNextBaklava(); + } else { + dispatchNextLegacy(); + } + } + + private void dispatchNextBaklava() { + assertTrue(isIdle()); + Message msg = mTestLooperManager.poll(); + if (msg == null) { + return; + } + msg.getTarget().dispatchMessage(msg); + } + + private synchronized void dispatchNextLegacy() { assertTrue(isIdle()); - Message msg = messageQueueNext(); + Message msg = messageQueueNextLegacy(); if (msg == null) { return; } diff --git a/tests/vcn/Android.bp b/tests/vcn/Android.bp index 51a300bff7ea..661ed07a5669 100644 --- a/tests/vcn/Android.bp +++ b/tests/vcn/Android.bp @@ -16,13 +16,19 @@ android_test { name: "FrameworksVcnTests", // For access hidden connectivity methods in tests defaults: ["framework-connectivity-test-defaults"], + + // TODO: b/374174952 Use 36 after Android B finalization + min_sdk_version: "35", + srcs: [ "java/**/*.java", "java/**/*.kt", ], platform_apis: true, - test_suites: ["device-tests"], - certificate: "platform", + test_suites: [ + "general-tests", + "mts-tethering", + ], static_libs: [ "android.net.vcn.flags-aconfig-java-export", "androidx.test.rules", diff --git a/tests/vcn/AndroidManifest.xml b/tests/vcn/AndroidManifest.xml index a8f657c89f76..08effbd1f7cf 100644 --- a/tests/vcn/AndroidManifest.xml +++ b/tests/vcn/AndroidManifest.xml @@ -16,8 +16,9 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.frameworks.tests.vcn"> - <uses-sdk android:minSdkVersion="33" - android:targetSdkVersion="33"/> + <!-- TODO: b/374174952 Use 36 after Android B finalization --> + <uses-sdk android:minSdkVersion="35" android:targetSdkVersion="35" /> + <application> <uses-library android:name="android.test.runner" /> </application> diff --git a/tests/vcn/AndroidTest.xml b/tests/vcn/AndroidTest.xml index dc521fd7bcd9..9c8362f36cb2 100644 --- a/tests/vcn/AndroidTest.xml +++ b/tests/vcn/AndroidTest.xml @@ -14,12 +14,20 @@ limitations under the License. --> <configuration description="Runs VCN Tests."> - <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> <option name="test-file-name" value="FrameworksVcnTests.apk" /> </target_preparer> <option name="test-suite-tag" value="apct" /> <option name="test-tag" value="FrameworksVcnTests" /> + + <!-- Run tests in MTS only if the Tethering Mainline module is installed. --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.tethering" /> + </object> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="com.android.frameworks.tests.vcn" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> diff --git a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java index 156961312323..0fa11ae1fe7d 100644 --- a/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java +++ b/tests/vcn/java/android/net/vcn/VcnCellUnderlyingNetworkTemplateTest.java @@ -23,11 +23,24 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Test; +import org.junit.runner.RunWith; import java.util.HashSet; import java.util.Set; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnCellUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase { private static final Set<String> ALLOWED_PLMN_IDS = new HashSet<>(); private static final Set<Integer> ALLOWED_CARRIER_IDS = new HashSet<>(); diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java index 73a0a6183cb6..fa97de0aff45 100644 --- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java @@ -29,11 +29,14 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.content.Context; +import android.os.Build; import android.os.Parcel; import android.util.ArraySet; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -42,7 +45,10 @@ import org.junit.runner.RunWith; import java.util.Collections; import java.util.Set; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnConfigTest { private static final String TEST_PACKAGE_NAME = VcnConfigTest.class.getPackage().getName(); diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java index 59dc68900100..990cc74caf6c 100644 --- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java @@ -34,10 +34,13 @@ import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeTunnelConnectionParams; import android.net.vcn.persistablebundleutils.IkeSessionParamsUtilsTest; import android.net.vcn.persistablebundleutils.TunnelConnectionParamsUtilsTest; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,7 +52,10 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionConfigTest { // Public for use in VcnGatewayConnectionTest diff --git a/tests/vcn/java/android/net/vcn/VcnManagerTest.java b/tests/vcn/java/android/net/vcn/VcnManagerTest.java index 8461de6d877b..1739fbc0fa6d 100644 --- a/tests/vcn/java/android/net/vcn/VcnManagerTest.java +++ b/tests/vcn/java/android/net/vcn/VcnManagerTest.java @@ -38,16 +38,28 @@ import android.net.NetworkCapabilities; import android.net.vcn.VcnManager.VcnStatusCallback; import android.net.vcn.VcnManager.VcnStatusCallbackBinder; import android.net.vcn.VcnManager.VcnUnderlyingNetworkPolicyListener; +import android.os.Build; import android.os.ParcelUuid; +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.net.UnknownHostException; import java.util.UUID; import java.util.concurrent.Executor; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnManagerTest { private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0)); private static final String GATEWAY_CONNECTION_NAME = "gatewayConnectionName"; diff --git a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java index 7bc9970629a6..52952eb3f2cc 100644 --- a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java +++ b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java @@ -30,12 +30,24 @@ import static org.junit.Assert.fail; import android.net.NetworkCapabilities; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; +import android.os.Build; import android.os.Parcel; +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Arrays; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnTransportInfoTest { private static final int SUB_ID = 1; private static final int NETWORK_ID = 5; diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java index a674425efea3..c82d2003dbf6 100644 --- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java +++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkPolicyTest.java @@ -22,9 +22,21 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import android.net.NetworkCapabilities; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; +import org.junit.runner.RunWith; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnUnderlyingNetworkPolicyTest { private static final VcnUnderlyingNetworkPolicy DEFAULT_NETWORK_POLICY = new VcnUnderlyingNetworkPolicy( diff --git a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java index 2110d6ee7c86..22361cc71f12 100644 --- a/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java +++ b/tests/vcn/java/android/net/vcn/VcnUnderlyingNetworkSpecifierTest.java @@ -22,14 +22,20 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.net.TelephonyNetworkSpecifier; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnUnderlyingNetworkSpecifierTest { private static final int[] TEST_SUB_IDS = new int[] {1, 2, 3, 5}; diff --git a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java index 3ce6c8f9386d..fb040d8f9b91 100644 --- a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java @@ -30,13 +30,25 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.TelephonyNetworkSpecifier; import android.net.wifi.WifiInfo; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Arrays; import java.util.Collections; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnUtilsTest { private static final int SUB_ID = 1; diff --git a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java index 4063178e005d..2c072e1cbc88 100644 --- a/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java +++ b/tests/vcn/java/android/net/vcn/VcnWifiUnderlyingNetworkTemplateTest.java @@ -22,10 +22,23 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Set; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnWifiUnderlyingNetworkTemplateTest extends VcnUnderlyingNetworkTemplateTestBase { private static final String SSID = "TestWifi"; diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java index bc8e9d3200b6..01e9ac2ac3cf 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/EapSessionConfigUtilsTest.java @@ -21,11 +21,14 @@ import static android.telephony.TelephonyManager.APPTYPE_USIM; import static org.junit.Assert.assertEquals; import android.net.eap.EapSessionConfig; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,7 +38,10 @@ import java.nio.charset.StandardCharsets; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class EapSessionConfigUtilsTest { private static final byte[] EAP_ID = "test@android.net".getBytes(StandardCharsets.US_ASCII); diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java index 4f3930f9b5af..821e5a6c94cb 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeIdentificationUtilsTest.java @@ -25,10 +25,13 @@ import android.net.ipsec.ike.IkeIpv4AddrIdentification; import android.net.ipsec.ike.IkeIpv6AddrIdentification; import android.net.ipsec.ike.IkeKeyIdIdentification; import android.net.ipsec.ike.IkeRfc822AddrIdentification; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,7 +42,10 @@ import java.net.InetAddress; import javax.security.auth.x500.X500Principal; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class IkeIdentificationUtilsTest { private static void verifyPersistableBundleEncodeDecodeIsLossless(IkeIdentification id) { diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java index 9f7d2390938f..7200aee1c012 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java @@ -29,14 +29,16 @@ import android.net.InetAddresses; import android.net.eap.EapSessionConfig; import android.net.ipsec.ike.IkeFqdnIdentification; import android.net.ipsec.ike.IkeSessionParams; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.org.bouncycastle.util.io.pem.PemObject; import com.android.internal.org.bouncycastle.util.io.pem.PemReader; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -52,7 +54,10 @@ import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.util.concurrent.TimeUnit; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class IkeSessionParamsUtilsTest { // Public for use in VcnGatewayConnectionConfigTest, EncryptedTunnelParamsUtilsTest diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java index 28cf38a2a583..957e785d70c0 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtilsTest.java @@ -20,17 +20,23 @@ import static org.junit.Assert.assertEquals; import android.net.InetAddresses; import android.net.ipsec.ike.IkeTrafficSelector; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; import java.net.InetAddress; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class IkeTrafficSelectorUtilsTest { private static final int START_PORT = 16; diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java index 664044a9e7d4..1e8f5ff2dc07 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/SaProposalUtilsTest.java @@ -21,15 +21,21 @@ import static org.junit.Assert.assertEquals; import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.IkeSaProposal; import android.net.ipsec.ike.SaProposal; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class SaProposalUtilsTest { /** Package private so that IkeSessionParamsUtilsTest can use it */ diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java index f9dc9eb4d5ae..7d17724112ec 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtilsTest.java @@ -20,14 +20,20 @@ import static org.junit.Assert.assertEquals; import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeTunnelConnectionParams; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class TunnelConnectionParamsUtilsTest { // Public for use in VcnGatewayConnectionConfigTest diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java index e0b5f0ef0381..3d7348a79b8c 100644 --- a/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtilsTest.java @@ -25,10 +25,13 @@ import android.net.InetAddresses; import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.IkeTrafficSelector; import android.net.ipsec.ike.TunnelModeChildSessionParams; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,7 +40,10 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.util.concurrent.TimeUnit; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class TunnelModeChildSessionParamsUtilsTest { // Package private for use in EncryptedTunnelParamsUtilsTest diff --git a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java index 47638b002f37..99c7aa72146b 100644 --- a/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/util/MtuUtilsTest.java @@ -33,9 +33,12 @@ import static org.junit.Assert.assertTrue; import static java.util.Collections.emptyList; import android.net.ipsec.ike.ChildSaProposal; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,7 +46,10 @@ import org.junit.runner.RunWith; import java.util.Arrays; import java.util.List; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class MtuUtilsTest { private void verifyUnderlyingMtuZero(boolean isIpv4) { diff --git a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java index c84e60086b37..f7786af840ee 100644 --- a/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java +++ b/tests/vcn/java/android/net/vcn/util/PersistableBundleUtilsTest.java @@ -21,10 +21,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,7 +38,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class PersistableBundleUtilsTest { private static final String TEST_KEY = "testKey"; diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 26a2a0636792..a97f9a837bab 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -79,6 +79,7 @@ import android.net.vcn.VcnManager; import android.net.vcn.VcnUnderlyingNetworkPolicy; import android.net.vcn.util.PersistableBundleUtils; import android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; +import android.os.Build; import android.os.IBinder; import android.os.ParcelUuid; import android.os.PersistableBundle; @@ -93,7 +94,6 @@ import android.telephony.TelephonyManager; import android.util.ArraySet; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.VcnManagementService.VcnCallback; import com.android.server.VcnManagementService.VcnStatusCallbackInfo; @@ -101,6 +101,8 @@ import com.android.server.vcn.TelephonySubscriptionTracker; import com.android.server.vcn.Vcn; import com.android.server.vcn.VcnContext; import com.android.server.vcn.VcnNetworkProvider; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Rule; @@ -117,8 +119,10 @@ import java.util.Map.Entry; import java.util.Set; import java.util.UUID; -/** Tests for {@link VcnManagementService}. */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnManagementServiceTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java index 77f82f0d8cf4..6276be27fbf5 100644 --- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java +++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java @@ -54,6 +54,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.vcn.VcnManager; +import android.os.Build; import android.os.Handler; import android.os.ParcelUuid; import android.os.PersistableBundle; @@ -69,9 +70,10 @@ import android.util.ArrayMap; import android.util.ArraySet; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.HandlerExecutor; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -87,8 +89,10 @@ import java.util.Map; import java.util.Set; import java.util.UUID; -/** Tests for TelephonySubscriptionTracker */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class TelephonySubscriptionTrackerTest { private static final String PACKAGE_NAME = diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index 74db6a5211a0..6608dda95a4b 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -70,16 +70,18 @@ import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnManager.VcnErrorCode; import android.net.vcn.VcnTransportInfo; import android.net.vcn.util.MtuUtils; +import android.os.Build; import android.os.PersistableBundle; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback; import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration; import com.android.server.vcn.VcnGatewayConnection.VcnIkeSession; import com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent; import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -94,8 +96,10 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; -/** Tests for VcnGatewayConnection.ConnectedState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase { private static final int PARALLEL_SA_COUNT = 4; diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java index 3c70759a2fa6..f6123d29f35a 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java @@ -26,17 +26,22 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.net.ipsec.ike.IkeSessionParams; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -/** Tests for VcnGatewayConnection.ConnectingState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionConnectingStateTest extends VcnGatewayConnectionTestBase { private VcnIkeSession mIkeSession; diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java index f3eb82f46de7..7cfaf5be5111 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectedStateTest.java @@ -30,16 +30,21 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.net.IpSecManager; +import android.os.Build; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for VcnGatewayConnection.DisconnectedState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionDisconnectedStateTest extends VcnGatewayConnectionTestBase { @Before diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java index 78aefad9f8ff..9132d830c54e 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java @@ -23,15 +23,21 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.os.Build; + import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for VcnGatewayConnection.DisconnectedState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionDisconnectingStateTest extends VcnGatewayConnectionTestBase { @Before diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java index 6568cdd44377..d5ef4e028709 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java @@ -27,15 +27,21 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.os.Build; + import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -/** Tests for VcnGatewayConnection.RetryTimeoutState */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionRetryTimeoutStateTest extends VcnGatewayConnectionTestBase { private long mFirstRetryInterval; diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java index b9fe76a24d20..5283322682ee 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java @@ -61,15 +61,17 @@ import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnManager; import android.net.vcn.VcnTransportInfo; import android.net.wifi.WifiInfo; +import android.os.Build; import android.os.ParcelUuid; import android.os.Process; import android.telephony.SubscriptionInfo; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -87,8 +89,10 @@ import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -/** Tests for TelephonySubscriptionTracker */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase { private static final int TEST_UID = Process.myUid() + 1; diff --git a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java index e9026e22b6b2..2b92428918db 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java @@ -29,12 +29,14 @@ import android.annotation.NonNull; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkRequest; +import android.os.Build; import android.os.test.TestLooper; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; @@ -44,8 +46,10 @@ import org.mockito.ArgumentCaptor; import java.util.ArrayList; import java.util.List; -/** Tests for TelephonySubscriptionTracker */ -@RunWith(AndroidJUnit4.class) +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SmallTest public class VcnNetworkProviderTest { private static final int TEST_SCORE_UNSATISFIED = 0; diff --git a/tests/vcn/java/com/android/server/vcn/VcnTest.java b/tests/vcn/java/com/android/server/vcn/VcnTest.java index 6d269686e42f..bd4aeba761da 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnTest.java @@ -49,20 +49,26 @@ import android.net.Uri; import android.net.vcn.VcnConfig; import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnGatewayConnectionConfigTest; +import android.os.Build; import android.os.ParcelUuid; import android.os.test.TestLooper; import android.provider.Settings; import android.telephony.TelephonyManager; import android.util.ArraySet; +import androidx.test.filters.SmallTest; + import com.android.server.VcnManagementService.VcnCallback; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.Vcn.VcnGatewayStatusCallback; import com.android.server.vcn.Vcn.VcnUserMobileDataStateListener; import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; @@ -73,6 +79,11 @@ import java.util.Map.Entry; import java.util.Set; import java.util.UUID; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class VcnTest { private static final String PKG_NAME = VcnTest.class.getPackage().getName(); private static final ParcelUuid TEST_SUB_GROUP = new ParcelUuid(new UUID(0, 0)); diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java index c11b6bb3435d..53a36d3e4d6a 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java @@ -44,16 +44,22 @@ import static org.mockito.Mockito.when; import android.content.BroadcastReceiver; import android.content.Intent; import android.net.IpSecTransformState; +import android.os.Build; import android.os.OutcomeReceiver; import android.os.PowerManager; +import androidx.test.filters.SmallTest; + import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculationResult; import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculator; import com.android.server.vcn.routeselection.NetworkMetricMonitor.IpSecTransformWrapper; import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -63,6 +69,11 @@ import java.util.Arrays; import java.util.BitSet; import java.util.concurrent.TimeUnit; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase { private static final String TAG = IpSecPacketLossDetectorTest.class.getSimpleName(); diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java index 4f34f9f8f74c..a9c637f7c943 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java @@ -42,16 +42,28 @@ import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnManager; import android.net.vcn.VcnUnderlyingNetworkTemplate; import android.net.vcn.VcnWifiUnderlyingNetworkTemplate; +import android.os.Build; import android.os.PersistableBundle; import android.util.ArraySet; +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Collections; import java.util.List; import java.util.Set; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class NetworkPriorityClassifierTest extends NetworkEvaluationTestBase { private UnderlyingNetworkRecord mWifiNetworkRecord; private UnderlyingNetworkRecord mCellNetworkRecord; diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java index e540932d0e1f..99c508c139ec 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java @@ -58,6 +58,7 @@ import android.net.vcn.VcnCellUnderlyingNetworkTemplate; import android.net.vcn.VcnCellUnderlyingNetworkTemplateTest; import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnUnderlyingNetworkTemplate; +import android.os.Build; import android.os.ParcelUuid; import android.os.test.TestLooper; import android.telephony.CarrierConfigManager; @@ -65,6 +66,8 @@ import android.telephony.SubscriptionInfo; import android.telephony.TelephonyManager; import android.util.ArraySet; +import androidx.test.filters.SmallTest; + import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.VcnContext; import com.android.server.vcn.VcnNetworkProvider; @@ -73,9 +76,12 @@ import com.android.server.vcn.routeselection.UnderlyingNetworkController.Network import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback; import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener; import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -89,6 +95,11 @@ import java.util.List; import java.util.Set; import java.util.UUID; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class UnderlyingNetworkControllerTest { private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0)); private static final int INITIAL_SUB_ID_1 = 1; diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java index a315b0690ec5..27c1bc105bde 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java @@ -38,19 +38,30 @@ import static org.mockito.Mockito.when; import android.net.IpSecTransform; import android.net.vcn.VcnGatewayConnectionConfig; +import android.os.Build; + +import androidx.test.filters.SmallTest; import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback; import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.Dependencies; import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import java.util.concurrent.TimeUnit; +// TODO: b/374174952 After B finalization, use Sdk36ModuleController to ensure VCN tests only run on +// Android B/B+ +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.VANILLA_ICE_CREAM) +@SmallTest public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase { private static final int PENALTY_TIMEOUT_MIN = 10; private static final long PENALTY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(PENALTY_TIMEOUT_MIN); diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index e24fe07f959b..9ef8b7dc9947 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -349,20 +349,22 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& value->value->Accept(&body_printer); printer->Undent(); } - printer->Println("Flag disabled values:"); - for (const auto& value : entry.flag_disabled_values) { - printer->Print("("); - printer->Print(value->config.to_string()); - printer->Print(") "); - value->value->Accept(&headline_printer); - if (options.show_sources && !value->value->GetSource().path.empty()) { - printer->Print(" src="); - printer->Print(value->value->GetSource().to_string()); + if (!entry.flag_disabled_values.empty()) { + printer->Println("Flag disabled values:"); + for (const auto& value : entry.flag_disabled_values) { + printer->Print("("); + printer->Print(value->config.to_string()); + printer->Print(") "); + value->value->Accept(&headline_printer); + if (options.show_sources && !value->value->GetSource().path.empty()) { + printer->Print(" src="); + printer->Print(value->value->GetSource().to_string()); + } + printer->Println(); + printer->Indent(); + value->value->Accept(&body_printer); + printer->Undent(); } - printer->Println(); - printer->Indent(); - value->value->Accept(&body_printer); - printer->Undent(); } printer->Undent(); } |